1mod error;
17mod sparse_range;
18
19use futures::{FutureExt, Stream, StreamExt};
20use http_content_range::{ContentRange, ContentRangeBytes};
21use reqwest::header::HeaderMap;
22use reqwest::{Response, Url};
23use sparse_range::SparseRange;
24use std::fmt::Debug;
25use std::ops::{Deref, DerefMut};
26use std::{
27 collections::BTreeMap,
28 fmt,
29 io::{self, SeekFrom},
30 ops::Range,
31 pin::Pin,
32 sync::{self, Arc},
33 task::{ready, Context, Poll},
34};
35use tokio::{
36 io::{AsyncRead, AsyncSeek, ReadBuf},
37 sync::watch::Sender,
38 sync::{watch, Mutex},
39};
40use tokio_stream::wrappers::WatchStream;
41use tokio_util::sync::PollSender;
42use tracing::{info_span, Instrument};
43
44pub use error::AsyncHttpRangeReaderError;
45
46#[derive(Debug)]
81pub struct AsyncHttpRangeReader {
82 inner: Mutex<Inner>,
83 len: u64,
84}
85
86#[derive(Default, Clone, Debug)]
87struct StreamerState {
88 resident_range: SparseRange,
89 requested_ranges: Vec<Range<u64>>,
90 error: Option<AsyncHttpRangeReaderError>,
91}
92
93#[derive(Default)]
95struct Chunks(BTreeMap<usize, Vec<u8>>);
96
97impl fmt::Debug for Chunks {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 let ranges = self
102 .0
103 .iter()
104 .map(|(start, bytes)| *start..*start + bytes.len());
105 f.debug_list().entries(ranges).finish()
106 }
107}
108
109impl Deref for Chunks {
110 type Target = BTreeMap<usize, Vec<u8>>;
111
112 fn deref(&self) -> &Self::Target {
113 &self.0
114 }
115}
116
117impl DerefMut for Chunks {
118 fn deref_mut(&mut self) -> &mut Self::Target {
119 &mut self.0
120 }
121}
122
123#[derive(Debug)]
128struct SharedCache {
129 chunks: sync::Mutex<Chunks>,
130 len: usize,
132}
133
134impl SharedCache {
135 fn new(len: u64) -> Result<Self, AsyncHttpRangeReaderError> {
136 let len = usize::try_from(len).map_err(|_| AsyncHttpRangeReaderError::FileTooLarge(len))?;
138 Ok(Self {
139 len,
140 chunks: sync::Mutex::new(Chunks::default()),
141 })
142 }
143
144 fn create_or_extend_chunk(
146 &self,
147 offset: usize,
148 bytes: &[u8],
149 ) -> Result<(), AsyncHttpRangeReaderError> {
150 let Ok(mut chunks) = self.chunks.lock() else {
151 return Err(AsyncHttpRangeReaderError::LockPoisoned);
152 };
153 let _new_len = {
154 let entry = chunks.entry(offset).or_default();
155 entry.extend_from_slice(bytes);
156 entry.len()
157 };
158 #[cfg(debug_assertions)]
160 {
161 if let Some((next_start, _)) = chunks
162 .range((
163 std::ops::Bound::Excluded(offset),
164 std::ops::Bound::Unbounded,
165 ))
166 .next()
167 {
168 assert!(offset + _new_len <= *next_start);
169 }
170 }
171 Ok(())
172 }
173
174 fn read(&self, range: Range<usize>, buf: &mut ReadBuf<'_>) -> io::Result<()> {
178 let chunks = self
179 .chunks
180 .lock()
181 .map_err(|_| io::Error::other(AsyncHttpRangeReaderError::LockPoisoned))?;
182 let (&first_stored_start, _) = chunks
183 .range(..=range.start)
184 .next_back()
185 .expect("resident range must have a cached chunk");
186 let mut offset = range.start;
187 for (&start, bytes) in chunks.range(first_stored_start..range.end) {
189 let end = range.end.min(start + bytes.len());
190 buf.put_slice(&bytes[offset - start..end - start]);
191 offset = end;
192 }
193 assert_eq!(offset, range.end, "resident range must be fully cached");
194 Ok(())
195 }
196}
197
198#[derive(Debug)]
199struct Inner {
200 data: Arc<SharedCache>,
202
203 pos: u64,
205
206 requested_range: SparseRange,
208
209 streamer_state: StreamerState,
211
212 streamer_state_rx: WatchStream<StreamerState>,
215
216 request_tx: tokio::sync::mpsc::Sender<Range<u64>>,
220
221 poll_request_tx: Option<PollSender<Range<u64>>>,
224}
225
226pub enum CheckSupportMethod {
230 NegativeRangeRequest(u64),
234
235 Head,
238}
239
240fn error_for_status(response: reqwest::Response) -> reqwest_middleware::Result<Response> {
241 response
242 .error_for_status()
243 .map_err(reqwest_middleware::Error::Reqwest)
244}
245
246impl AsyncHttpRangeReader {
247 pub async fn new(
249 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
250 url: Url,
251 check_method: CheckSupportMethod,
252 extra_headers: HeaderMap,
253 ) -> Result<(Self, HeaderMap), AsyncHttpRangeReaderError> {
254 let client = client.into();
255 match check_method {
256 CheckSupportMethod::NegativeRangeRequest(initial_chunk_size) => {
257 let response = Self::initial_tail_request(
258 client.clone(),
259 url.clone(),
260 initial_chunk_size,
261 HeaderMap::default(),
262 )
263 .await?;
264 let response_headers = response.headers().clone();
265 let self_ = Self::from_range_response(client, response, url, extra_headers).await?;
266 Ok((self_, response_headers))
267 }
268 CheckSupportMethod::Head => {
269 let response =
270 Self::initial_head_request(client.clone(), url.clone(), HeaderMap::default())
271 .await?;
272 let response_headers = response.headers().clone();
273 let self_ = Self::from_head_response(client, response, url, extra_headers).await?;
274 Ok((self_, response_headers))
275 }
276 }
277 }
278
279 pub async fn initial_tail_request(
283 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
284 url: reqwest::Url,
285 initial_chunk_size: u64,
286 extra_headers: HeaderMap,
287 ) -> Result<Response, AsyncHttpRangeReaderError> {
288 let client = client.into();
289 let tail_response = client
290 .get(url)
291 .header(
292 reqwest::header::RANGE,
293 format!("bytes=-{initial_chunk_size}"),
294 )
295 .headers(extra_headers)
296 .send()
297 .await
298 .and_then(error_for_status)
299 .map_err(Arc::new)
300 .map_err(AsyncHttpRangeReaderError::HttpError)?;
301 Ok(tail_response)
302 }
303
304 #[deprecated(note = "use `from_range_response` instead")]
305 pub async fn from_tail_response(
306 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
307 tail_request_response: Response,
308 url: Url,
309 extra_headers: HeaderMap,
310 ) -> Result<Self, AsyncHttpRangeReaderError> {
311 Self::from_range_response(client, tail_request_response, url, extra_headers).await
312 }
313
314 pub async fn from_range_response(
317 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
318 response: Response,
319 url: Url,
320 extra_headers: HeaderMap,
321 ) -> Result<Self, AsyncHttpRangeReaderError> {
322 let client = client.into();
323
324 let content_range_header = response
326 .headers()
327 .get(reqwest::header::CONTENT_RANGE)
328 .ok_or(AsyncHttpRangeReaderError::ContentRangeMissing)?
329 .to_str()
330 .map_err(|_err| AsyncHttpRangeReaderError::ContentRangeMissing)?;
331 let content_range = ContentRange::parse(content_range_header).ok_or_else(|| {
333 AsyncHttpRangeReaderError::ContentRangeParser(content_range_header.to_string())
334 })?;
335 let (start, end_inclusive, complete_length) = match content_range {
336 ContentRange::Bytes(ContentRangeBytes {
337 first_byte,
338 last_byte,
339 complete_length,
340 }) => (first_byte, last_byte, complete_length),
341 _ => return Err(AsyncHttpRangeReaderError::HttpRangeRequestUnsupported),
342 };
343
344 let data = Arc::new(SharedCache::new(complete_length)?);
345
346 let requested_range = SparseRange::from_range(start..end_inclusive + 1);
347
348 let (request_tx, request_rx) = tokio::sync::mpsc::channel(10);
353 let (state_tx, state_rx) = watch::channel(StreamerState::default());
354 tokio::spawn(run_streamer(
355 client,
356 url,
357 extra_headers,
358 Some((response, start, end_inclusive + 1)),
359 Arc::clone(&data),
360 state_tx,
361 request_rx,
362 ));
363
364 let mut streamer_state = StreamerState::default();
366 streamer_state
367 .requested_ranges
368 .push(start..end_inclusive + 1);
369
370 let reader = Self {
371 len: data.len as u64,
372 inner: Mutex::new(Inner {
373 data,
374 pos: 0,
375 requested_range,
376 streamer_state,
377 streamer_state_rx: WatchStream::new(state_rx),
378 request_tx,
379 poll_request_tx: None,
380 }),
381 };
382 Ok(reader)
383 }
384
385 pub async fn initial_head_request(
388 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
389 url: reqwest::Url,
390 extra_headers: HeaderMap,
391 ) -> Result<Response, AsyncHttpRangeReaderError> {
392 let client = client.into();
393
394 let head_response = client
396 .head(url.clone())
397 .headers(extra_headers)
398 .send()
399 .await
400 .and_then(error_for_status)
401 .map_err(Arc::new)
402 .map_err(AsyncHttpRangeReaderError::HttpError)?;
403 Ok(head_response)
404 }
405
406 pub async fn from_head_response(
409 client: impl Into<reqwest_middleware::ClientWithMiddleware>,
410 head_response: Response,
411 url: Url,
412 extra_headers: HeaderMap,
413 ) -> Result<Self, AsyncHttpRangeReaderError> {
414 let client = client.into();
415
416 if head_response
418 .headers()
419 .get(reqwest::header::ACCEPT_RANGES)
420 .and_then(|h| h.to_str().ok())
421 != Some("bytes")
422 {
423 return Err(AsyncHttpRangeReaderError::HttpRangeRequestUnsupported);
424 }
425
426 let content_length: u64 = head_response
427 .headers()
428 .get(reqwest::header::CONTENT_LENGTH)
429 .ok_or(AsyncHttpRangeReaderError::ContentLengthMissing)?
430 .to_str()
431 .map_err(|_err| AsyncHttpRangeReaderError::ContentLengthMissing)?
432 .parse()
433 .map_err(|_err| AsyncHttpRangeReaderError::ContentLengthMissing)?;
434
435 let data = Arc::new(SharedCache::new(content_length)?);
436
437 let requested_range = SparseRange::default();
438
439 let (request_tx, request_rx) = tokio::sync::mpsc::channel(10);
444 let (state_tx, state_rx) = watch::channel(StreamerState::default());
445 tokio::spawn(run_streamer(
446 client,
447 url,
448 extra_headers,
449 None,
450 Arc::clone(&data),
451 state_tx,
452 request_rx,
453 ));
454
455 let streamer_state = StreamerState::default();
457
458 let reader = Self {
459 len: data.len as u64,
460 inner: Mutex::new(Inner {
461 data,
462 pos: 0,
463 requested_range,
464 streamer_state,
465 streamer_state_rx: WatchStream::new(state_rx),
466 request_tx,
467 poll_request_tx: None,
468 }),
469 };
470 Ok(reader)
471 }
472
473 pub async fn requested_ranges(&self) -> Vec<Range<u64>> {
475 let mut inner = self.inner.lock().await;
476 if let Some(Some(new_state)) = inner.streamer_state_rx.next().now_or_never() {
477 inner.streamer_state = new_state;
478 }
479 inner.streamer_state.requested_ranges.clone()
480 }
481
482 pub async fn prefetch(&mut self, bytes: Range<u64>) {
485 let inner = self.inner.get_mut();
486
487 let range = bytes.start..(bytes.end.min(inner.data.len as u64));
489 if range.start >= range.end {
490 return;
491 }
492
493 let inner = self.inner.get_mut();
495 if let Some((new_range, _)) = inner.requested_range.cover(range.clone()) {
496 let _ = inner.request_tx.send(range).await;
497 inner.requested_range = new_range;
498 }
499 }
500
501 #[allow(clippy::len_without_is_empty)]
503 pub fn len(&self) -> u64 {
504 self.len
505 }
506}
507
508#[tracing::instrument(name = "fetch_ranges", skip_all, fields(url))]
511async fn run_streamer(
512 client: reqwest_middleware::ClientWithMiddleware,
513 url: Url,
514 extra_headers: HeaderMap,
515 response: Option<(Response, u64, u64)>,
516 data: Arc<SharedCache>,
517 mut state_tx: Sender<StreamerState>,
518 mut request_rx: tokio::sync::mpsc::Receiver<Range<u64>>,
519) {
520 let mut state = StreamerState::default();
521
522 if let Some((response, start, end_exclusive)) = response {
523 state.requested_ranges.push(start..end_exclusive);
525
526 if !stream_response(
528 response,
529 start,
530 end_exclusive,
531 &data,
532 &mut state_tx,
533 &mut state,
534 )
535 .await
536 {
537 return;
538 }
539 }
540
541 'outer: loop {
543 let range = match request_rx.recv().await {
544 Some(range) => range,
545 None => {
546 break 'outer;
547 }
548 };
549
550 let uncovered_ranges = match state.resident_range.cover(range) {
552 None => continue,
553 Some((_, uncovered_ranges)) => uncovered_ranges,
554 };
555
556 for range in uncovered_ranges {
558 state
560 .requested_ranges
561 .push(*range.start()..*range.end() + 1);
562
563 let range_string = format!("bytes={}-{}", range.start(), range.end());
565 let span = info_span!("fetch_range", range = range_string.as_str());
566 let response = match client
567 .get(url.clone())
568 .header(reqwest::header::RANGE, range_string)
569 .headers(extra_headers.clone())
570 .send()
571 .instrument(span)
572 .await
573 .and_then(error_for_status)
574 .map_err(std::io::Error::other)
575 {
576 Err(e) => {
577 state.error = Some(e.into());
578 let _ = state_tx.send(state);
579 break 'outer;
580 }
581 Ok(response) => response,
582 };
583
584 if let Err(err) =
585 validate_content_range(&response, *range.start(), *range.end(), data.len)
586 {
587 state.error = Some(err);
588 let _ = state_tx.send(state);
589 break 'outer;
590 }
591
592 if response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
595 state.error = Some(AsyncHttpRangeReaderError::HttpRangeRequestUnsupported);
596 let _ = state_tx.send(state);
597 break 'outer;
598 }
599
600 if !stream_response(
601 response,
602 *range.start(),
603 *range.end() + 1,
604 &data,
605 &mut state_tx,
606 &mut state,
607 )
608 .await
609 {
610 break 'outer;
611 }
612 }
613 }
614}
615
616fn validate_content_range(
618 response: &Response,
619 expected_start: u64,
620 expected_end_inclusive: u64,
621 expected_complete_length: usize,
622) -> Result<(), AsyncHttpRangeReaderError> {
623 let content_range_header = response
624 .headers()
625 .get(reqwest::header::CONTENT_RANGE)
626 .ok_or(AsyncHttpRangeReaderError::ContentRangeMissing)?
627 .to_str()
628 .map_err(|_err| AsyncHttpRangeReaderError::ContentRangeMissing)?;
629 let content_range = ContentRange::parse(content_range_header).ok_or_else(|| {
630 AsyncHttpRangeReaderError::ContentRangeParser(content_range_header.to_string())
631 })?;
632 let (actual_start, actual_end_inclusive, actual_complete_length) = match content_range {
633 ContentRange::Bytes(ContentRangeBytes {
634 first_byte,
635 last_byte,
636 complete_length,
637 }) => (first_byte, last_byte, complete_length),
638 _ => return Err(AsyncHttpRangeReaderError::HttpRangeRequestUnsupported),
639 };
640 if expected_start != actual_start
641 || expected_end_inclusive != actual_end_inclusive
642 || expected_complete_length as u64 != actual_complete_length
643 {
644 return Err(AsyncHttpRangeReaderError::RangeMismatch {
645 expected_start,
646 expected_end_inclusive,
647 expected_complete_length,
648 actual_start,
649 actual_end_inclusive,
650 actual_complete_length,
651 });
652 }
653
654 Ok(())
655}
656
657async fn stream_response(
663 tail_request_response: Response,
664 start: u64,
665 end_exclusive: u64,
666 data: &SharedCache,
667 state_tx: &mut Sender<StreamerState>,
668 state: &mut StreamerState,
669) -> bool {
670 assert!(
672 end_exclusive <= data.len as u64,
673 "end is outside of file {} > {}",
674 end_exclusive,
675 data.len
676 );
677
678 let mut offset = start;
679 let mut byte_stream = tail_request_response.bytes_stream();
680 while let Some(bytes) = byte_stream.next().await {
681 let bytes = match bytes {
682 Err(e) => {
683 state.error = Some(e.into());
684 let _ = state_tx.send(state.clone());
685 return false;
686 }
687 Ok(bytes) => bytes,
688 };
689
690 if bytes.is_empty() {
691 continue;
692 }
693
694 if bytes.len() as u64 > end_exclusive - offset {
696 state.error = Some(AsyncHttpRangeReaderError::ResponseTooLong {
697 expected: end_exclusive - start,
698 });
699 let _ = state_tx.send(state.clone());
700 return false;
701 }
702
703 offset += bytes.len() as u64;
704
705 match data.create_or_extend_chunk(start as usize, &bytes) {
707 Ok(()) => {}
708 Err(err) => {
709 state.error = Some(err);
710 let _ = state_tx.send(state.clone());
711 return false;
712 }
713 }
714
715 state.resident_range.update(start..offset);
717
718 if state_tx.send(state.clone()).is_err() {
720 return false;
723 }
724 }
725
726 if offset != end_exclusive {
728 state.error = Some(AsyncHttpRangeReaderError::ResponseTooShort {
729 expected: end_exclusive - start,
730 actual: offset - start,
731 });
732 let _ = state_tx.send(state.clone());
733 return false;
734 }
735
736 true
737}
738
739impl AsyncSeek for AsyncHttpRangeReader {
740 fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> {
741 let me = self.get_mut();
742 let inner = me.inner.get_mut();
743
744 inner.pos = match position {
745 SeekFrom::Start(pos) => Some(pos),
746 SeekFrom::End(relative) => (inner.data.len as u64).checked_add_signed(relative),
747 SeekFrom::Current(relative) => inner.pos.checked_add_signed(relative),
748 }
749 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek position"))?;
750
751 Ok(())
752 }
753
754 fn poll_complete(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<u64>> {
755 let inner = self.inner.get_mut();
756 Poll::Ready(Ok(inner.pos))
757 }
758}
759
760impl AsyncRead for AsyncHttpRangeReader {
761 fn poll_read(
762 self: Pin<&mut Self>,
763 cx: &mut Context<'_>,
764 buf: &mut ReadBuf<'_>,
765 ) -> Poll<io::Result<()>> {
766 let me = self.get_mut();
767 let inner = me.inner.get_mut();
768
769 if let Some(e) = inner.streamer_state.error.as_ref() {
771 return Poll::Ready(Err(io::Error::other(e.clone())));
772 }
773
774 let range = inner.pos
776 ..inner
777 .pos
778 .saturating_add(buf.remaining() as u64)
779 .min(inner.data.len as u64);
780 if range.start >= range.end {
781 return Poll::Ready(Ok(()));
782 }
783
784 while !inner.requested_range.is_covered(range.clone()) {
786 if let Some(mut poll) = inner.poll_request_tx.take() {
788 match poll.poll_reserve(cx) {
789 Poll::Ready(_) => {
790 let _ = poll.send_item(range.clone());
791 inner.requested_range.update(range.clone());
792 break;
793 }
794 Poll::Pending => {
795 inner.poll_request_tx = Some(poll);
796 return Poll::Pending;
797 }
798 }
799 }
800
801 inner.poll_request_tx = Some(PollSender::new(inner.request_tx.clone()));
803 }
804
805 if let Some(mut poll) = inner.poll_request_tx.take() {
807 poll.abort_send();
808 }
809
810 loop {
811 if inner
813 .streamer_state
814 .resident_range
815 .is_covered(range.clone())
816 {
817 inner
818 .data
819 .read(range.start as usize..range.end as usize, buf)?;
820 inner.pos = range.end;
821 return Poll::Ready(Ok(()));
822 }
823
824 match ready!(Pin::new(&mut inner.streamer_state_rx).poll_next(cx)) {
826 None => {
827 let error = AsyncHttpRangeReaderError::DownloadTaskStopped;
828 inner.streamer_state.error = Some(error.clone());
829 return Poll::Ready(Err(io::Error::other(error)));
830 }
831 Some(state) => {
832 inner.streamer_state = state;
833 if let Some(e) = inner.streamer_state.error.as_ref() {
834 return Poll::Ready(Err(io::Error::other(e.clone())));
835 }
836 }
837 }
838 }
839 }
840}
841
842#[cfg(test)]
843mod static_directory_server;
844
845#[cfg(test)]
846mod lifetime_tests;
847
848#[cfg(test)]
849mod test {
850 use super::*;
851 use crate::static_directory_server::StaticDirectoryServer;
852 use assert_matches::assert_matches;
853 use async_zip::tokio::read::seek::ZipFileReader;
854 use axum::body::Body;
855 use axum::extract::Request;
856 use axum::response::IntoResponse;
857 use futures::AsyncReadExt;
858 use reqwest::header;
859 use reqwest::Method;
860 use reqwest::{Client, StatusCode};
861 use rstest::*;
862 use std::path::Path;
863 use std::time::Duration;
864 use tokio::io::AsyncReadExt as _;
865
866 #[test]
867 fn sparse_cache_reads_across_chunks() {
868 let cache = SharedCache::new(isize::MAX as u64).unwrap();
870 let start = cache.len - 16;
871 cache.create_or_extend_chunk(start + 8, b"89ab").unwrap();
872 cache.create_or_extend_chunk(0, b"other range").unwrap();
873 cache.create_or_extend_chunk(start, b"0123").unwrap();
874 cache.create_or_extend_chunk(start + 4, b"4567").unwrap();
875
876 let mut output = [b'?'; 10];
877 let mut buf = ReadBuf::new(&mut output);
878 buf.put_slice(b">");
879 cache.read(start + 2..start + 10, &mut buf).unwrap();
880 assert_eq!(buf.filled(), b">23456789");
881 assert_eq!(&output, b">23456789?");
882 }
883
884 #[tokio::test]
885 async fn closed_download_stream_returns_terminal_error() {
886 let response = axum::http::Response::builder()
887 .header(header::ACCEPT_RANGES, "bytes")
888 .header(header::CONTENT_LENGTH, "4")
889 .body("")
890 .unwrap();
891 let mut reader = AsyncHttpRangeReader::from_head_response(
892 Client::builder().no_proxy().build().unwrap(),
893 response.into(),
894 Url::parse("http://localhost/file").unwrap(),
895 HeaderMap::new(),
896 )
897 .await
898 .unwrap();
899
900 let (state_tx, state_rx) = watch::channel(StreamerState::default());
901 drop(state_tx);
902 let inner = reader.inner.get_mut();
903 inner.streamer_state_rx = WatchStream::new(state_rx);
904 inner.requested_range = SparseRange::from_range(0..4);
906
907 let mut contents = [0; 4];
908 let error = tokio::time::timeout(Duration::from_secs(5), reader.read_exact(&mut contents))
909 .await
910 .unwrap()
911 .expect_err("a closed download stream must not leave a read pending");
912 assert_matches!(
913 error
914 .get_ref()
915 .and_then(|error| error.downcast_ref::<AsyncHttpRangeReaderError>()),
916 Some(AsyncHttpRangeReaderError::DownloadTaskStopped)
917 );
918
919 reader
921 .inner
922 .get_mut()
923 .streamer_state
924 .resident_range
925 .update(0..4);
926 let repeated_error = reader
927 .read_exact(&mut contents)
928 .await
929 .expect_err("the terminal error must be retained");
930 assert_eq!(repeated_error.to_string(), error.to_string());
931 }
932
933 #[rstest]
934 #[case(CheckSupportMethod::Head)]
935 #[case(CheckSupportMethod::NegativeRangeRequest(8192))]
936 #[tokio::test]
937 async fn async_range_reader_zip(#[case] check_method: CheckSupportMethod) {
938 let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("test-data");
940 let server = StaticDirectoryServer::new(&path)
941 .await
942 .expect("could not initialize server");
943
944 let filepath = path.join("andes-1.8.3-pyhd8ed1ab_0.conda");
946 assert!(
947 filepath.exists(),
948 "The conda package is not there yet. Did you run `git lfs pull`?"
949 );
950 let file_size = std::fs::metadata(&filepath).unwrap().len();
951 assert_eq!(
952 file_size, 2_463_995,
953 "The conda package is not there yet. Did you run `git lfs pull`?"
954 );
955
956 let (mut range, _) = AsyncHttpRangeReader::new(
958 Client::new(),
959 server.url().join("andes-1.8.3-pyhd8ed1ab_0.conda").unwrap(),
960 check_method,
961 HeaderMap::default(),
962 )
963 .await
964 .expect("Could not download range - did you run `git lfs pull`?");
965
966 range.prefetch(range.len() - 8192..range.len()).await;
968
969 assert_eq!(range.len(), file_size);
970
971 let mut reader = ZipFileReader::with_tokio(tokio::io::BufReader::with_capacity(0, range))
972 .await
973 .unwrap();
974
975 assert_eq!(
976 reader
977 .file()
978 .entries()
979 .iter()
980 .map(|e| e.filename().as_str().unwrap_or(""))
981 .collect::<Vec<_>>(),
982 vec![
983 "metadata.json",
984 "info-andes-1.8.3-pyhd8ed1ab_0.tar.zst",
985 "pkg-andes-1.8.3-pyhd8ed1ab_0.tar.zst",
986 ]
987 );
988
989 let request_ranges = reader
991 .inner_mut()
992 .get_mut()
993 .get_mut()
994 .requested_ranges()
995 .await;
996 assert_eq!(request_ranges.len(), 1);
997 assert_eq!(
998 request_ranges[0].end - request_ranges[0].start,
999 8192,
1000 "first request should be the size of the initial chunk size"
1001 );
1002 assert_eq!(
1003 request_ranges[0].end, file_size,
1004 "first request should be at the end"
1005 );
1006
1007 let entry = reader.file().entries().first().unwrap();
1009 let offset = entry.header_offset();
1010 let size = entry.compressed_size() + 30 + entry.filename().as_bytes().len() as u64;
1013
1014 let buffer_size = 8192;
1017 let size = size.div_ceil(buffer_size) * buffer_size;
1018
1019 reader
1021 .inner_mut()
1022 .get_mut()
1023 .get_mut()
1024 .prefetch(offset..offset + size as u64)
1025 .await;
1026
1027 let mut contents = String::new();
1029 reader
1030 .reader_with_entry(0)
1031 .await
1032 .unwrap()
1033 .read_to_string(&mut contents)
1034 .await
1035 .unwrap();
1036
1037 let request_ranges = reader
1039 .inner_mut()
1040 .get_mut()
1041 .get_mut()
1042 .requested_ranges()
1043 .await;
1044
1045 assert_eq!(contents, r#"{"conda_pkg_format_version": 2}"#);
1046 assert_eq!(request_ranges.len(), 2);
1047 assert_eq!(
1048 request_ranges[1],
1049 0..size,
1050 "expected only two range requests"
1051 );
1052 }
1053
1054 #[rstest]
1055 #[case(CheckSupportMethod::Head)]
1056 #[case(CheckSupportMethod::NegativeRangeRequest(8192))]
1057 #[tokio::test]
1058 async fn async_range_reader(#[case] check_method: CheckSupportMethod) {
1059 let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("test-data");
1061 let server = StaticDirectoryServer::new(&path)
1062 .await
1063 .expect("could not initialize server");
1064
1065 let (mut range, _) = AsyncHttpRangeReader::new(
1067 Client::new(),
1068 server.url().join("andes-1.8.3-pyhd8ed1ab_0.conda").unwrap(),
1069 check_method,
1070 HeaderMap::default(),
1071 )
1072 .await
1073 .expect("bla");
1074
1075 let mut file = tokio::fs::File::open(path.join("andes-1.8.3-pyhd8ed1ab_0.conda"))
1077 .await
1078 .unwrap();
1079
1080 let mut range_read = vec![0; 64 * 1024];
1082 let mut file_read = vec![0; 64 * 1024];
1083 loop {
1084 let range_read_bytes = range.read(&mut range_read).await.unwrap();
1086
1087 let file_read_bytes = file
1089 .read_exact(&mut file_read[0..range_read_bytes])
1090 .await
1091 .unwrap();
1092
1093 assert_eq!(range_read_bytes, file_read_bytes);
1094 assert_eq!(
1095 range_read[0..range_read_bytes],
1096 file_read[0..file_read_bytes]
1097 );
1098
1099 if file_read_bytes == 0 && range_read_bytes == 0 {
1100 break;
1101 }
1102 }
1103 }
1104
1105 #[tokio::test]
1106 async fn test_not_found() {
1107 let server = StaticDirectoryServer::new(Path::new(env!("CARGO_MANIFEST_DIR")))
1108 .await
1109 .expect("could not initialize server");
1110 let err = AsyncHttpRangeReader::new(
1111 Client::new(),
1112 server.url().join("not-found").unwrap(),
1113 CheckSupportMethod::Head,
1114 HeaderMap::default(),
1115 )
1116 .await
1117 .expect_err("expected an error");
1118
1119 assert_matches!(
1120 err, AsyncHttpRangeReaderError::HttpError(err) if err.status() == Some(StatusCode::NOT_FOUND)
1121 );
1122 }
1123
1124 async fn spawn_mismatch_server(
1127 head_content_length: usize,
1128 pretend_size: usize,
1129 actual_size: usize,
1130 ) -> Url {
1131 let app =
1132 axum::Router::new().fallback(async move |request: Request| match *request.method() {
1133 Method::HEAD => {
1134 let headers = [
1135 (header::CONTENT_LENGTH, head_content_length.to_string()),
1136 (header::ACCEPT_RANGES, "bytes".to_string()),
1137 ];
1138 (StatusCode::OK, headers).into_response()
1139 }
1140 Method::GET => {
1141 let range_header = request
1142 .headers()
1143 .get(header::RANGE)
1144 .unwrap()
1145 .to_str()
1146 .unwrap()
1147 .to_string();
1148
1149 let range_spec = range_header.strip_prefix("bytes=").unwrap();
1150 let (start_str, _end_str) = range_spec.split_once('-').unwrap();
1151 let start = start_str.parse::<usize>().unwrap();
1152 let end = start + pretend_size - 1;
1154
1155 axum::response::Response::builder()
1156 .status(StatusCode::PARTIAL_CONTENT)
1157 .header(
1160 header::CONTENT_RANGE,
1161 format!("bytes {start}-{end}/{head_content_length}"),
1162 )
1163 .body(Body::from(vec![1u8; actual_size]))
1164 .unwrap()
1165 .into_response()
1166 }
1167 _ => StatusCode::METHOD_NOT_ALLOWED.into_response(),
1168 });
1169
1170 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
1171 let local_addr = listener.local_addr().unwrap();
1172 tokio::spawn(async move {
1173 axum::serve(listener, app.into_make_service())
1174 .await
1175 .unwrap();
1176 });
1177
1178 Url::parse(&format!("http://localhost:{}/file", local_addr.port())).unwrap()
1179 }
1180
1181 #[tokio::test]
1183 async fn test_content_length_response_beyond_content_length() {
1184 fn into_range_error(err: std::io::Error) -> AsyncHttpRangeReaderError {
1186 err.into_inner()
1187 .unwrap()
1188 .downcast::<AsyncHttpRangeReaderError>()
1189 .map(|e| *e)
1190 .unwrap()
1191 }
1192
1193 let cases: Vec<(usize, usize, usize, Option<AsyncHttpRangeReaderError>)> = vec![
1194 (512, 512, 512, None),
1196 (
1198 512,
1199 512,
1200 1024,
1201 Some(AsyncHttpRangeReaderError::ResponseTooLong { expected: 512 }),
1202 ),
1203 (
1205 512,
1206 1024,
1207 1024,
1208 Some(AsyncHttpRangeReaderError::ContentRangeParser(
1209 "bytes 0-1023/512".to_string(),
1210 )),
1211 ),
1212 (
1214 512,
1215 1024,
1216 512,
1217 Some(AsyncHttpRangeReaderError::ContentRangeParser(
1218 "bytes 0-1023/512".to_string(),
1219 )),
1220 ),
1221 (1024, 512, 512, None),
1223 (
1225 1024,
1226 512,
1227 1024,
1228 Some(AsyncHttpRangeReaderError::ResponseTooLong { expected: 512 }),
1229 ),
1230 (
1232 1024,
1233 1024,
1234 1024,
1235 Some(AsyncHttpRangeReaderError::RangeMismatch {
1236 expected_start: 0,
1237 expected_end_inclusive: 511,
1238 expected_complete_length: 1024,
1239 actual_start: 0,
1240 actual_end_inclusive: 1023,
1241 actual_complete_length: 1024,
1242 }),
1243 ),
1244 (
1246 1024,
1247 1024,
1248 512,
1249 Some(AsyncHttpRangeReaderError::RangeMismatch {
1250 expected_start: 0,
1251 expected_end_inclusive: 511,
1252 expected_complete_length: 1024,
1253 actual_start: 0,
1254 actual_end_inclusive: 1023,
1255 actual_complete_length: 1024,
1256 }),
1257 ),
1258 ];
1259 for (head_content_length, range_header_length, range_actual_length, expected_error) in cases
1260 {
1261 let url = spawn_mismatch_server(
1262 head_content_length,
1263 range_header_length,
1264 range_actual_length,
1265 )
1266 .await;
1267
1268 let (mut reader, _) = AsyncHttpRangeReader::new(
1269 Client::new(),
1270 url,
1271 CheckSupportMethod::Head,
1272 HeaderMap::default(),
1273 )
1274 .await
1275 .unwrap();
1276
1277 assert_eq!(reader.len(), head_content_length as u64);
1278 reader.prefetch(0..512).await;
1279
1280 let mut buf = vec![0u8; 512];
1281 let result = reader.read(&mut buf).await;
1282 let label =
1283 format!("{head_content_length} {range_header_length} {range_actual_length}");
1284 match expected_error {
1285 None => {
1286 assert_matches!(result, Ok(_), "{label}");
1287 }
1288 Some(expected) => {
1289 assert_eq!(
1291 into_range_error(result.unwrap_err()).to_string(),
1292 expected.to_string(),
1293 "{label}"
1294 );
1295 }
1296 }
1297 }
1298 }
1299}