1use std::io::Read;
44use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
45use std::sync::{Mutex, OnceLock};
46use std::time::Duration;
47
48use crate::RangeReader;
49
50pub const WHOLE_BODY_CAP: u64 = 256 << 20;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct FetchPolicy {
58 pub concurrency: usize,
62 pub coalesce_gap: u64,
66 pub max_span: u64,
69 pub retries: u32,
72 pub backoff_ms: u64,
74 pub trace: bool,
77 pub whole_body_cap: u64,
80 pub open_read: u64,
83 pub whole_max: u64,
87}
88
89pub const WHOLE_FILE_MAX: u64 = 8 << 20;
93
94impl FetchPolicy {
95 pub const DEFAULT: FetchPolicy = FetchPolicy {
99 concurrency: 16,
100 coalesce_gap: 32 << 10,
101 max_span: 8 << 20,
102 retries: 2,
103 backoff_ms: 100,
104 trace: false,
105 whole_body_cap: WHOLE_BODY_CAP,
106 open_read: OPEN_READ,
107 whole_max: WHOLE_FILE_MAX,
108 };
109
110 pub fn from_env() -> Self {
112 let num = |k: &str| {
113 std::env::var(k)
114 .ok()
115 .and_then(|v| v.trim().parse::<u64>().ok())
116 };
117 let d = Self::DEFAULT;
118 FetchPolicy {
119 concurrency: num("RETE_HTTP_CONCURRENCY")
120 .map_or(d.concurrency, |n| n.clamp(1, 64) as usize),
121 coalesce_gap: num("RETE_HTTP_COALESCE_KB")
122 .map_or(d.coalesce_gap, |kb| kb.saturating_mul(1024)),
123 max_span: d.max_span,
124 retries: num("RETE_HTTP_RETRIES").map_or(d.retries, |n| n.min(10) as u32),
125 backoff_ms: d.backoff_ms,
126 trace: std::env::var_os("RETE_HTTP_TRACE").is_some(),
127 whole_body_cap: d.whole_body_cap,
128 open_read: d.open_read,
129 whole_max: num("RETE_HTTP_WHOLE_MB")
130 .map_or(d.whole_max, |mb| mb.saturating_mul(1 << 20)),
131 }
132 }
133
134 fn backoff(&self, attempt: u32) -> Duration {
135 let ms = self
136 .backoff_ms
137 .saturating_mul(4u64.saturating_pow(attempt))
138 .min(2000);
139 Duration::from_millis(ms)
140 }
141}
142
143impl Default for FetchPolicy {
144 fn default() -> Self {
145 Self::DEFAULT
146 }
147}
148
149enum Failure {
151 Transient(std::io::Error),
152 Permanent(std::io::Error),
153}
154
155fn retryable_status(code: u16) -> bool {
158 code == 408 || code == 429 || ((500..600).contains(&code) && code != 501 && code != 505)
159}
160
161fn classify(err: ureq::Error, what: &str) -> Failure {
162 match err {
163 ureq::Error::Status(code, _) => {
164 let e = std::io::Error::other(format!("HTTP {code} for {what}"));
165 if retryable_status(code) {
166 Failure::Transient(e)
167 } else {
168 Failure::Permanent(e)
169 }
170 }
171 ureq::Error::Transport(t) => {
172 let transient = matches!(
173 t.kind(),
174 ureq::ErrorKind::Io
175 | ureq::ErrorKind::ConnectionFailed
176 | ureq::ErrorKind::Dns
177 | ureq::ErrorKind::ProxyConnect
178 );
179 let e = std::io::Error::other(format!("{t} ({what})"));
180 if transient {
181 Failure::Transient(e)
182 } else {
183 Failure::Permanent(e)
184 }
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191struct ContentRange {
192 start: u64,
193 end: u64,
195 total: Option<u64>,
197}
198
199fn parse_content_range(v: &str) -> Option<ContentRange> {
203 let rest = v.trim().strip_prefix("bytes ")?.trim_start();
204 let (range, total) = rest.split_once('/')?;
205 let (a, b) = range.split_once('-')?;
206 let num = |x: &str| -> Option<u64> {
207 let x = x.trim();
208 (!x.is_empty() && x.bytes().all(|c| c.is_ascii_digit()))
209 .then(|| x.parse().ok())
210 .flatten()
211 };
212 let (start, end) = (num(a)?, num(b)?);
213 let total = match total.trim() {
214 "*" => None,
215 t => Some(num(t)?),
216 };
217 if start > end || total.is_some_and(|t| end >= t) {
218 return None;
219 }
220 Some(ContentRange { start, end, total })
221}
222
223fn content_range_total(v: &str) -> Option<u64> {
225 parse_content_range(v)?.total
226}
227
228#[derive(Debug, PartialEq, Eq)]
230struct Span {
231 offset: u64,
232 len: u64,
233}
234
235type Placement = Vec<Option<(usize, u64)>>;
238
239fn coalesce(
244 ranges: &[(u64, u64)],
245 gap: u64,
246 max_span: u64,
247) -> std::io::Result<(Vec<Span>, Placement)> {
248 let mut order: Vec<usize> = (0..ranges.len()).filter(|&i| ranges[i].1 > 0).collect();
249 for &i in &order {
250 let (o, l) = ranges[i];
251 if o.checked_add(l).is_none() {
252 return Err(std::io::Error::new(
253 std::io::ErrorKind::InvalidInput,
254 format!("range {o}+{l} overflows"),
255 ));
256 }
257 }
258 order.sort_by_key(|&i| ranges[i]);
259 let mut spans: Vec<Span> = Vec::new();
260 let mut place: Placement = vec![None; ranges.len()];
261 for i in order {
262 let (o, l) = ranges[i];
263 let end = o + l;
264 let joins = spans.last().is_some_and(|s| {
265 let s_end = s.offset + s.len;
266 o <= s_end.saturating_add(gap) && end.max(s_end) - s.offset <= max_span
267 });
268 if joins {
269 let s = spans.last_mut().expect("checked above");
270 s.len = (s.offset + s.len).max(end) - s.offset;
271 } else {
272 spans.push(Span { offset: o, len: l });
273 }
274 let si = spans.len() - 1;
275 place[i] = Some((si, o - spans[si].offset));
276 }
277 Ok((spans, place))
278}
279
280fn whole_body_refusal(url: &str, bytes: Option<u64>, cap: u64) -> std::io::Error {
283 let size = match bytes {
284 Some(n) => format!("all {n} bytes"),
285 None => format!("more than {cap} bytes"),
286 };
287 std::io::Error::other(format!(
288 "the host ignored the Range header and sent {size} of {url} (status 200 instead \
289 of 206 Partial Content); a full body over {} MiB is not kept in memory — host the \
290 file where HTTP range requests work",
291 cap >> 20
292 ))
293}
294
295fn slice_whole(whole: &[u8], offset: u64, len: u64, url: &str) -> std::io::Result<Vec<u8>> {
298 let end = offset
299 .checked_add(len)
300 .filter(|&end| end <= whole.len() as u64)
301 .ok_or_else(|| {
302 std::io::Error::other(format!(
303 "range {offset}+{len} is past the end of the {} bytes {url} sent",
304 whole.len()
305 ))
306 })?;
307 Ok(whole[offset as usize..end as usize].to_vec())
308}
309
310fn read_capped(resp: ureq::Response, cap: u64, what: &str) -> Result<Option<Vec<u8>>, Failure> {
313 let declared = resp
314 .header("content-length")
315 .and_then(|v| v.trim().parse::<u64>().ok());
316 if declared.is_some_and(|n| n > cap) {
317 return Ok(None); }
319 let mut buf = Vec::with_capacity(declared.unwrap_or(0).min(cap) as usize);
320 resp.into_reader()
321 .take(cap.saturating_add(1))
322 .read_to_end(&mut buf)
323 .map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
324 if buf.len() as u64 > cap {
325 return Ok(None);
326 }
327 if declared.is_some_and(|n| n != buf.len() as u64) {
328 return Err(Failure::Transient(std::io::Error::other(format!(
329 "short body: got {} of the {} bytes {what} declared",
330 buf.len(),
331 declared.unwrap_or(0)
332 ))));
333 }
334 Ok(Some(buf))
335}
336
337struct Probe {
340 len: u64,
341 whole: Option<Vec<u8>>,
342 prefix: Option<Vec<u8>>,
345}
346
347pub const OPEN_READ: u64 = 64 * 1024;
350
351pub struct HttpRangeReader {
355 url: String,
356 len: u64,
357 agent: ureq::Agent,
358 policy: FetchPolicy,
359 headers: Vec<(String, String)>,
361 whole: OnceLock<Vec<u8>>,
364 adopting: Mutex<()>,
367 prefix: Vec<u8>,
370}
371
372impl HttpRangeReader {
373 pub fn open(url: &str) -> std::io::Result<Self> {
375 Self::open_with(url, FetchPolicy::from_env())
376 }
377
378 pub fn open_with(url: &str, policy: FetchPolicy) -> std::io::Result<Self> {
380 Self::open_with_headers(url, Vec::new(), policy)
381 }
382
383 pub fn open_with_headers(
389 url: &str,
390 headers: Vec<(String, String)>,
391 policy: FetchPolicy,
392 ) -> std::io::Result<Self> {
393 let agent = ureq::AgentBuilder::new()
394 .max_idle_connections(policy.concurrency.max(4) * 2)
396 .max_idle_connections_per_host(policy.concurrency)
397 .timeout_connect(Duration::from_secs(30))
398 .timeout_read(Duration::from_secs(120))
399 .build();
400 let mut reader = HttpRangeReader {
401 url: url.to_string(),
402 len: 0,
403 agent,
404 policy,
405 headers,
406 whole: OnceLock::new(),
407 adopting: Mutex::new(()),
408 prefix: Vec::new(),
409 };
410 let probe = reader.probe_len()?;
411 reader.len = probe.len;
412 if let Some(body) = probe.whole {
413 let _ = reader.whole.set(body);
414 }
415 if let Some(p) = probe.prefix {
416 reader.prefix = p;
417 }
418 let held = reader.prefix.len() as u64;
420 if reader.whole.get().is_none()
421 && held > 0
422 && reader.len > held
423 && reader.len <= reader.policy.whole_max
424 {
425 let rest = reader.with_retries(|| reader.fetch_once(held, reader.len - held))?;
426 let mut body = std::mem::take(&mut reader.prefix);
427 body.extend_from_slice(&rest);
428 if reader.policy.trace {
429 eprintln!("[http] {}: {} bytes, fetched whole", reader.url, body.len());
430 }
431 let _ = reader.whole.set(body);
432 }
433 Ok(reader)
434 }
435
436 pub fn url(&self) -> &str {
438 &self.url
439 }
440
441 pub fn serves_whole_body(&self) -> bool {
444 self.whole.get().is_some()
445 }
446
447 fn request(&self, method: &str) -> ureq::Request {
448 let mut req = self.agent.request(method, &self.url);
449 for (k, v) in &self.headers {
450 req = req.set(k, v);
451 }
452 req
453 }
454
455 fn with_retries<T>(&self, mut call: impl FnMut() -> Result<T, Failure>) -> std::io::Result<T> {
457 let mut attempt = 0;
458 loop {
459 match call() {
460 Ok(v) => return Ok(v),
461 Err(Failure::Permanent(e)) => return Err(e),
462 Err(Failure::Transient(e)) if attempt >= self.policy.retries => {
463 return Err(if attempt == 0 {
464 e
465 } else {
466 std::io::Error::other(format!("{e} (after {} attempts)", attempt + 1))
467 });
468 }
469 Err(Failure::Transient(_)) => {
470 std::thread::sleep(self.policy.backoff(attempt));
471 attempt += 1;
472 }
473 }
474 }
475 }
476
477 fn probe_len(&self) -> std::io::Result<Probe> {
481 let open_read = self.policy.open_read;
485 let first_what = format!("bytes 0-{} of {}", open_read.saturating_sub(1), self.url);
486 let first_cap = self.policy.whole_body_cap;
487 let first = self.with_retries(|| {
488 if open_read == 0 {
489 return Ok(None);
490 }
491 let resp = match self
492 .request("GET")
493 .set("Range", &format!("bytes=0-{}", open_read - 1))
494 .call()
495 {
496 Ok(r) => r,
497 Err(ureq::Error::Status(code, _)) if !retryable_status(code) => {
500 return Ok(None);
501 }
502 Err(e) => return Err(classify(e, &first_what)),
503 };
504 match resp.status() {
505 206 => {
506 let Some(cr) = resp.header("content-range").and_then(parse_content_range)
510 else {
511 return Ok(None);
512 };
513 let Some(len) = cr.total.filter(|_| cr.start == 0) else {
514 return Ok(None);
515 };
516 let want = open_read.min(len);
517 if cr.end + 1 != want {
518 return Err(Failure::Permanent(std::io::Error::other(format!(
519 "{first_what}: the server answered Content-Range bytes {}-{}/{len}, \
520 not the {want} bytes asked for",
521 cr.start, cr.end
522 ))));
523 }
524 let mut body = Vec::new();
525 resp.into_reader()
526 .take(want + 1)
527 .read_to_end(&mut body)
528 .map_err(Failure::Transient)?;
529 if body.len() as u64 > want {
530 return Err(Failure::Permanent(std::io::Error::other(format!(
531 "{first_what}: the body is longer than its Content-Range"
532 ))));
533 }
534 if (body.len() as u64) < want {
535 return Err(Failure::Transient(std::io::Error::other(format!(
536 "short body for {first_what}"
537 ))));
538 }
539 Ok(Some(Probe {
540 len,
541 whole: None,
542 prefix: Some(body),
543 }))
544 }
545 200 => match read_capped(resp, first_cap, &first_what)? {
546 Some(body) if body.is_empty() => Ok(None),
547 Some(body) => Ok(Some(Probe {
548 len: body.len() as u64,
549 whole: Some(body),
550 prefix: None,
551 })),
552 None => Err(Failure::Permanent(whole_body_refusal(
553 &self.url, None, first_cap,
554 ))),
555 },
556 _ => Ok(None),
557 }
558 })?;
559 if let Some(p) = first {
560 return Ok(p);
561 }
562 let head_what = format!("HEAD {}", self.url);
563 let head = self.with_retries(|| {
564 self.request("HEAD")
565 .call()
566 .map_err(|e| classify(e, &head_what))
567 });
568 let head_err = match head {
569 Ok(resp) => match resp
570 .header("content-length")
571 .and_then(|v| v.trim().parse::<u64>().ok())
572 {
573 Some(len) => {
574 return Ok(Probe {
575 len,
576 whole: None,
577 prefix: None,
578 })
579 }
580 None => "no Content-Length".to_string(),
581 },
582 Err(e) => e.to_string(),
583 };
584 let what = format!("bytes 0-0 of {}", self.url);
585 let cap = self.policy.whole_body_cap;
586 let probe = self.with_retries(|| {
587 let resp = self
588 .request("GET")
589 .set("Range", "bytes=0-0")
590 .call()
591 .map_err(|e| classify(e, &what))?;
592 match resp.status() {
593 206 => Ok(resp
594 .header("content-range")
595 .and_then(content_range_total)
596 .map(|len| Probe {
597 len,
598 whole: None,
599 prefix: None,
600 })),
601 200 => match read_capped(resp, cap, &what)? {
602 Some(body) if body.is_empty() => {
603 Err(Failure::Permanent(std::io::Error::other(format!(
604 "{} answered 200 with an empty body",
605 self.url
606 ))))
607 }
608 Some(body) => Ok(Some(Probe {
609 len: body.len() as u64,
610 whole: Some(body),
611 prefix: None,
612 })),
613 None => Err(Failure::Permanent(whole_body_refusal(&self.url, None, cap))),
614 },
615 s => Err(Failure::Permanent(std::io::Error::other(format!(
616 "status {s} for {what}"
617 )))),
618 }
619 })?;
620 probe.ok_or_else(|| {
621 std::io::Error::other(format!(
622 "could not determine the file length of {}: {head_err} on HEAD, and no \
623 Content-Range total on a ranged GET",
624 self.url
625 ))
626 })
627 }
628
629 fn prefix_slice(&self, offset: u64, len: u64) -> Option<Vec<u8>> {
631 let end = offset.checked_add(len)?;
632 (end <= self.prefix.len() as u64)
633 .then(|| self.prefix[offset as usize..end as usize].to_vec())
634 }
635
636 fn bounds(&self, offset: u64, len: u64) -> std::io::Result<()> {
639 if offset.checked_add(len).is_none_or(|end| end > self.len) {
640 return Err(std::io::Error::new(
641 std::io::ErrorKind::UnexpectedEof,
642 format!(
643 "range out of bounds: {offset}+{len} past the {} bytes {} reported",
644 self.len, self.url
645 ),
646 ));
647 }
648 Ok(())
649 }
650
651 fn adopt_whole(&self, resp: ureq::Response, what: &str) -> Result<(), Failure> {
655 let _guard = self.adopting.lock().unwrap_or_else(|p| p.into_inner());
656 if self.whole.get().is_some() {
657 return Ok(());
658 }
659 let cap = self.policy.whole_body_cap;
660 let declared = resp
661 .header("content-length")
662 .and_then(|v| v.trim().parse::<u64>().ok());
663 if let Some(n) = declared {
664 if n > cap {
665 return Err(Failure::Permanent(whole_body_refusal(
666 &self.url,
667 Some(n),
668 cap,
669 )));
670 }
671 if n != self.len {
672 return Err(Failure::Permanent(std::io::Error::other(format!(
673 "the host ignored the Range header and sent {n} bytes of {}, which is not \
674 the {} bytes it reported for the file",
675 self.url, self.len
676 ))));
677 }
678 }
679 let body = read_capped(resp, cap, what)?
680 .ok_or_else(|| Failure::Permanent(whole_body_refusal(&self.url, None, cap)))?;
681 if body.len() as u64 != self.len {
682 return Err(Failure::Permanent(std::io::Error::other(format!(
683 "the host ignored the Range header and sent {} bytes of {}, which is not the \
684 {} bytes it reported for the file",
685 body.len(),
686 self.url,
687 self.len
688 ))));
689 }
690 if self.policy.trace {
691 eprintln!(
692 "[http] {} ignored Range: every read is served from its {}-byte body",
693 self.url,
694 body.len()
695 );
696 }
697 let _ = self.whole.set(body);
698 Ok(())
699 }
700
701 fn fetch_once(&self, offset: u64, len: u64) -> Result<Vec<u8>, Failure> {
703 let end = offset + len - 1; let what = format!("bytes {offset}-{end} of {}", self.url);
705 let resp = self
706 .request("GET")
707 .set("Range", &format!("bytes={offset}-{end}"))
708 .call()
709 .map_err(|e| classify(e, &what))?;
710 if resp.status() == 200 {
714 self.adopt_whole(resp, &what)?;
715 let whole = self.whole.get().expect("adopted above");
716 return slice_whole(whole, offset, len, &self.url).map_err(Failure::Permanent);
717 }
718 if resp.status() != 206 {
719 return Err(Failure::Permanent(std::io::Error::other(format!(
720 "server answered status {} (expected 206 Partial Content) for {what}; \
721 the host must support HTTP range requests",
722 resp.status()
723 ))));
724 }
725 if let Some(v) = resp.header("content-range") {
729 let Some(cr) = parse_content_range(v) else {
730 return Err(Failure::Permanent(std::io::Error::other(format!(
731 "server answered a malformed Content-Range `{v}` for {what}"
732 ))));
733 };
734 if cr.start != offset || cr.end != end {
735 return Err(Failure::Permanent(std::io::Error::other(format!(
736 "server answered Content-Range bytes {}-{} for a request of bytes \
737 {offset}-{end} ({})",
738 cr.start, cr.end, self.url
739 ))));
740 }
741 if let Some(t) = cr.total.filter(|&t| t != self.len) {
742 return Err(Failure::Permanent(std::io::Error::other(format!(
743 "{} is now {t} bytes, not the {} it was when opened: the file changed \
744 under the same URL",
745 self.url, self.len
746 ))));
747 }
748 }
749 let size = usize::try_from(len).map_err(|_| {
750 Failure::Permanent(std::io::Error::other(format!(
751 "a {len}-byte range is too large for memory"
752 )))
753 })?;
754 let mut buf = Vec::with_capacity(size);
755 resp.into_reader()
758 .take(len + 1)
759 .read_to_end(&mut buf)
760 .map_err(|e| Failure::Transient(std::io::Error::other(format!("{e} ({what})"))))?;
761 if (buf.len() as u64) < len {
765 return Err(Failure::Transient(std::io::Error::other(format!(
766 "short range response: got {} of {len} bytes at offset {offset} from {}",
767 buf.len(),
768 self.url
769 ))));
770 }
771 if buf.len() > size {
773 return Err(Failure::Permanent(std::io::Error::other(format!(
774 "overlong range response: more than {len} bytes for {what}"
775 ))));
776 }
777 Ok(buf)
778 }
779
780 fn fetch(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
783 if let Some(whole) = self.whole.get() {
784 return slice_whole(whole, offset, len, &self.url);
785 }
786 self.with_retries(|| self.fetch_once(offset, len))
787 }
788
789 fn fetch_spans(&self, spans: &[Span]) -> std::io::Result<Vec<Vec<u8>>> {
791 let workers = self.policy.concurrency.min(spans.len());
792 if workers <= 1 {
793 return spans.iter().map(|s| self.fetch(s.offset, s.len)).collect();
794 }
795 let next = AtomicUsize::new(0);
796 let failed = AtomicBool::new(false);
797 type Slot = Mutex<Option<std::io::Result<Vec<u8>>>>;
798 let slots: Vec<Slot> = spans.iter().map(|_| Mutex::new(None)).collect();
799 let panicked = std::thread::scope(|scope| {
800 let handles: Vec<_> = (0..workers)
801 .map(|_| {
802 scope.spawn(|| loop {
803 if failed.load(Ordering::Relaxed) {
804 break; }
806 let i = next.fetch_add(1, Ordering::Relaxed);
807 let Some(s) = spans.get(i) else { break };
808 let r = self.fetch(s.offset, s.len);
809 if r.is_err() {
810 failed.store(true, Ordering::Relaxed);
811 }
812 *slots[i].lock().unwrap_or_else(|p| p.into_inner()) = Some(r);
813 })
814 })
815 .collect();
816 handles
817 .into_iter()
818 .map(|h| h.join().is_err())
819 .fold(false, |a, b| a | b)
820 });
821 if panicked {
822 return Err(std::io::Error::other("read_many worker panicked"));
823 }
824 let mut out = Vec::with_capacity(spans.len());
825 let mut first_err = None;
826 for slot in slots {
827 match slot.into_inner().unwrap_or_else(|p| p.into_inner()) {
828 Some(Ok(b)) => out.push(b),
829 Some(Err(e)) => {
830 first_err.get_or_insert(e);
831 }
832 None => {} }
834 }
835 match first_err {
836 Some(e) => Err(e),
837 None if out.len() == spans.len() => Ok(out),
838 None => Err(std::io::Error::other("read_many left a range unfetched")),
839 }
840 }
841}
842
843impl RangeReader for HttpRangeReader {
844 fn len(&self) -> u64 {
845 self.len
846 }
847
848 fn read_at(&self, offset: u64, len: u64) -> std::io::Result<Vec<u8>> {
849 if len == 0 {
850 return Ok(Vec::new());
851 }
852 self.bounds(offset, len)?;
853 if let Some(b) = self.prefix_slice(offset, len) {
854 return Ok(b);
855 }
856 if self.policy.trace && self.whole.get().is_none() {
857 eprintln!("[http] read {offset}+{len}");
858 }
859 self.fetch(offset, len)
860 }
861
862 fn concurrency(&self) -> usize {
865 self.policy.concurrency
866 }
867
868 fn read_many(&self, ranges: &[(u64, u64)]) -> std::io::Result<Vec<Vec<u8>>> {
871 for &(o, l) in ranges {
872 if l > 0 {
873 self.bounds(o, l)?;
874 }
875 }
876 if let Some(whole) = self.whole.get() {
877 return ranges
878 .iter()
879 .map(|&(o, l)| slice_whole(whole, o, l, &self.url))
880 .collect();
881 }
882 if ranges
883 .iter()
884 .any(|&(o, l)| l > 0 && self.prefix_slice(o, l).is_some())
885 {
886 let rest: Vec<(u64, u64)> = ranges
888 .iter()
889 .copied()
890 .filter(|&(o, l)| l > 0 && self.prefix_slice(o, l).is_none())
891 .collect();
892 let mut fetched = if rest.is_empty() {
893 Vec::new()
894 } else {
895 self.read_many(&rest)?
896 }
897 .into_iter();
898 return ranges
899 .iter()
900 .map(|&(o, l)| {
901 if l == 0 {
902 Ok(Vec::new())
903 } else if let Some(b) = self.prefix_slice(o, l) {
904 Ok(b)
905 } else {
906 fetched.next().ok_or_else(|| {
907 std::io::Error::other("read_many left a range unfetched")
908 })
909 }
910 })
911 .collect();
912 }
913 let (spans, place) = coalesce(ranges, self.policy.coalesce_gap, self.policy.max_span)?;
914 if self.policy.trace {
915 let bytes: u64 = spans.iter().map(|s| s.len).sum();
916 eprintln!(
917 "[http] batch of {} range(s) -> {} request(s), {bytes} bytes, {} in flight",
918 ranges.len(),
919 spans.len(),
920 self.policy.concurrency.min(spans.len())
921 );
922 }
923 let blobs = self.fetch_spans(&spans)?;
924 Ok(place
925 .into_iter()
926 .zip(ranges)
927 .map(|(p, &(_, len))| match p {
928 None => Vec::new(),
929 Some((si, at)) => {
930 let at = at as usize;
931 blobs[si][at..at + len as usize].to_vec()
932 }
933 })
934 .collect())
935 }
936}
937
938#[cfg(test)]
939mod tests {
940 use super::*;
941 use std::io::Write;
942 use std::net::TcpListener;
943 use std::sync::atomic::AtomicU64;
944 use std::sync::Arc;
945
946 #[derive(Clone, Copy, Debug)]
948 enum ServerMode {
949 HonorRange,
951 IgnoreRange,
953 IgnoreRangeUnsized,
956 HeadWithoutLength,
959 HeadWithoutLengthIgnoreRange,
962 TruncateBody,
965 Error500,
967 NotFound,
969 Unavailable(u64),
971 WrongOffset,
973 WrongEnd,
975 WrongTotal,
978 Overlong,
980 FailAt(usize),
982 RequireHeader,
985 }
986
987 #[derive(Default)]
997 struct Stats {
998 gets: AtomicU64,
999 in_flight: AtomicU64,
1000 max_in_flight: AtomicU64,
1001 }
1002
1003 fn serve_with(data: Vec<u8>, mode: ServerMode, delay: Duration) -> (String, Arc<Stats>) {
1008 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1009 let url = format!("http://{}/", listener.local_addr().unwrap());
1010 let stats = Arc::new(Stats::default());
1011 let data = Arc::new(data);
1012 let st = stats.clone();
1013 std::thread::spawn(move || {
1014 for stream in listener.incoming() {
1015 let Ok(stream) = stream else { continue };
1016 let (data, st) = (data.clone(), st.clone());
1017 std::thread::spawn(move || handle(stream, &data, mode, delay, &st));
1018 }
1019 });
1020 (url, stats)
1021 }
1022
1023 fn serve(data: Vec<u8>, mode: ServerMode) -> String {
1024 serve_with(data, mode, Duration::ZERO).0
1025 }
1026
1027 fn handle(
1028 mut stream: std::net::TcpStream,
1029 data: &[u8],
1030 mode: ServerMode,
1031 delay: Duration,
1032 st: &Stats,
1033 ) {
1034 let mut req = Vec::new();
1036 let mut byte = [0u8; 1];
1037 while !req.ends_with(b"\r\n\r\n") {
1038 use std::io::Read as _;
1039 if stream.read(&mut byte).unwrap_or(0) == 0 {
1040 break;
1041 }
1042 req.push(byte[0]);
1043 }
1044 let text = String::from_utf8_lossy(&req);
1045 let is_head = text.starts_with("HEAD");
1046 let range = text.lines().find_map(|l| {
1047 let l = l.to_ascii_lowercase();
1048 l.strip_prefix("range: bytes=")
1049 .map(|r| r.trim().to_string())
1050 });
1051 let authorized = text
1052 .lines()
1053 .any(|l| l.eq_ignore_ascii_case("x-token: secret"));
1054
1055 let total = data.len();
1056 let status_only = |stream: &mut std::net::TcpStream, line: &str| {
1057 let _ = stream.write_all(
1058 format!("HTTP/1.1 {line}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
1059 .as_bytes(),
1060 );
1061 };
1062 if matches!(mode, ServerMode::RequireHeader) && !authorized {
1063 status_only(&mut stream, "401 Unauthorized");
1064 return;
1065 }
1066 if is_head {
1067 let h = if matches!(
1068 mode,
1069 ServerMode::HeadWithoutLength | ServerMode::HeadWithoutLengthIgnoreRange
1070 ) {
1071 "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n".to_string()
1072 } else {
1073 format!(
1074 "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nAccept-Ranges: bytes\r\nConnection: close\r\n\r\n"
1075 )
1076 };
1077 let _ = stream.write_all(h.as_bytes());
1078 return;
1079 }
1080 let n = st.gets.fetch_add(1, Ordering::SeqCst) + 1;
1081 let now = st.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
1082 st.max_in_flight.fetch_max(now, Ordering::SeqCst);
1083 std::thread::sleep(delay);
1084 st.in_flight.fetch_sub(1, Ordering::SeqCst);
1088 let ignores_range = matches!(
1089 mode,
1090 ServerMode::IgnoreRange
1091 | ServerMode::IgnoreRangeUnsized
1092 | ServerMode::HeadWithoutLengthIgnoreRange
1093 );
1094 match (mode, range) {
1095 (ServerMode::Error500, _) => status_only(&mut stream, "500 Internal Server Error"),
1096 (ServerMode::NotFound, _) => status_only(&mut stream, "404 Not Found"),
1097 (ServerMode::Unavailable(k), _) if n <= k => {
1098 status_only(&mut stream, "503 Service Unavailable")
1099 }
1100 (ServerMode::FailAt(at), Some(r)) if r.starts_with(&format!("{at}-")) => {
1101 status_only(&mut stream, "404 Not Found")
1102 }
1103 (ServerMode::IgnoreRangeUnsized, _) => {
1104 let _ = stream.write_all(b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n");
1105 let _ = stream.write_all(data);
1106 }
1107 (_, Some(r)) if !ignores_range => {
1108 let (a, b) = r.split_once('-').unwrap();
1109 let a: usize = a.parse().unwrap();
1110 let b: usize = b.parse().unwrap();
1111 let slice = &data[a.min(total)..=b.min(total - 1)];
1112 let claimed = if matches!(mode, ServerMode::WrongOffset) {
1113 a + 1
1114 } else {
1115 a
1116 };
1117 let last = b.min(total - 1);
1120 let claimed_end = if matches!(mode, ServerMode::WrongEnd) {
1121 last + 1
1122 } else {
1123 last
1124 };
1125 let claimed_total = if matches!(mode, ServerMode::WrongTotal) {
1126 total + 10
1127 } else {
1128 total
1129 };
1130 let extra = usize::from(matches!(mode, ServerMode::Overlong));
1131 let h = format!(
1132 "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes {claimed}-{claimed_end}/{claimed_total}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1133 slice.len() + extra
1134 );
1135 let _ = stream.write_all(h.as_bytes());
1136 let sent = if matches!(mode, ServerMode::TruncateBody) {
1137 &slice[..slice.len() / 2] } else {
1139 slice
1140 };
1141 let _ = stream.write_all(sent);
1142 if extra > 0 {
1143 let _ = stream.write_all(b"!");
1144 }
1145 }
1146 _ => {
1148 let h = format!(
1149 "HTTP/1.1 200 OK\r\nContent-Length: {total}\r\nConnection: close\r\n\r\n"
1150 );
1151 let _ = stream.write_all(h.as_bytes());
1152 let _ = stream.write_all(data);
1153 }
1154 }
1155 }
1156
1157 fn quick() -> FetchPolicy {
1161 FetchPolicy {
1162 backoff_ms: 1,
1163 open_read: 0,
1164 ..FetchPolicy::DEFAULT
1165 }
1166 }
1167
1168 #[test]
1171 fn the_open_reads_first_and_sends_no_head() {
1172 let data: Vec<u8> = (0..=255u8).cycle().take(100_000).collect();
1173 let (url, stats) = serve_with(data.clone(), ServerMode::HeadWithoutLength, Duration::ZERO);
1174 let policy = FetchPolicy {
1175 backoff_ms: 1,
1176 whole_max: 0,
1177 ..FetchPolicy::DEFAULT
1178 };
1179 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1180 assert_eq!(r.len(), 100_000);
1181 assert_eq!(stats.gets.load(Ordering::SeqCst), 1, "the open is one GET");
1182 assert_eq!(r.read_at(1000, 24).unwrap(), &data[1000..1024]);
1183 assert_eq!(
1184 r.read_many(&[(0, 16), (60_000, 8)]).unwrap()[1],
1185 &data[60_000..60_008]
1186 );
1187 assert_eq!(
1188 stats.gets.load(Ordering::SeqCst),
1189 1,
1190 "served from the first read"
1191 );
1192 assert_eq!(r.read_at(70_000, 4).unwrap(), &data[70_000..70_004]);
1193 assert_eq!(
1194 stats.gets.load(Ordering::SeqCst),
1195 2,
1196 "past it: one more GET"
1197 );
1198 let small: Vec<u8> = (0..100u8).collect();
1200 let (url, stats) = serve_with(small.clone(), ServerMode::HonorRange, Duration::ZERO);
1201 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1202 assert_eq!(r.len(), 100);
1203 assert_eq!(r.read_at(90, 10).unwrap(), &small[90..]);
1204 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
1205 }
1206
1207 #[test]
1208 fn reads_exact_ranges_from_a_range_host() {
1209 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
1210 let url = serve(data.clone(), ServerMode::HonorRange);
1211 let r = HttpRangeReader::open(&url).unwrap();
1212 assert_eq!(r.len(), 1000);
1213 assert_eq!(r.read_at(0, 4).unwrap(), &data[0..4]);
1214 assert_eq!(r.read_at(500, 20).unwrap(), &data[500..520]);
1215 assert_eq!(r.read_at(996, 4).unwrap(), &data[996..1000]);
1216 assert!(r.read_at(0, 0).unwrap().is_empty());
1217 assert!(!r.serves_whole_body());
1218 }
1219
1220 #[test]
1224 #[ignore = "hits the network (httpbin); run with --ignored"]
1225 fn reads_ranges_over_https() {
1226 let url = "https://httpbin.org/range/2048";
1227 let r = HttpRangeReader::open(url).unwrap();
1228 assert!(r.len() >= 2048);
1229 let chunk = r.read_at(100, 16).unwrap();
1230 assert_eq!(chunk.len(), 16);
1231 }
1232
1233 #[test]
1238 fn keeps_the_whole_body_of_a_host_that_ignores_range() {
1239 let data: Vec<u8> = (0..=255u8).cycle().take(5000).collect();
1240 let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRange, Duration::ZERO);
1241 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1242 assert_eq!(r.len(), 5000);
1243 assert_eq!(r.read_at(50, 10).unwrap(), &data[50..60]);
1244 assert!(r.serves_whole_body());
1245 assert_eq!(r.read_at(4990, 10).unwrap(), &data[4990..5000]);
1246 let got = r.read_many(&[(0, 4), (3000, 100), (7, 0)]).unwrap();
1247 assert_eq!(got[0], &data[0..4]);
1248 assert_eq!(got[1], &data[3000..3100]);
1249 assert!(got[2].is_empty());
1250 assert_eq!(
1251 stats.gets.load(Ordering::SeqCst),
1252 1,
1253 "one download, then memory"
1254 );
1255 assert!(r.read_at(4999, 2).is_err());
1257 }
1258
1259 #[test]
1262 fn keeps_an_unsized_whole_body_too() {
1263 let data: Vec<u8> = (0..=255u8).cycle().take(3000).collect();
1264 let (url, stats) = serve_with(data.clone(), ServerMode::IgnoreRangeUnsized, Duration::ZERO);
1265 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1266 assert_eq!(
1267 r.read_many(&[(10, 5), (2000, 5)]).unwrap()[1],
1268 &data[2000..2005]
1269 );
1270 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
1271 }
1272
1273 #[test]
1276 fn refuses_a_whole_body_over_the_cap() {
1277 let small_cap = FetchPolicy {
1278 whole_body_cap: 1000,
1279 ..quick()
1280 };
1281 let data: Vec<u8> = (0..=255u8).cycle().take(4000).collect();
1282 for mode in [ServerMode::IgnoreRange, ServerMode::IgnoreRangeUnsized] {
1283 let (url, stats) = serve_with(data.clone(), mode, Duration::ZERO);
1284 let r = HttpRangeReader::open_with(&url, small_cap).unwrap();
1285 let err = r.read_at(50, 10).unwrap_err().to_string();
1286 assert!(
1287 err.contains("ignored the Range header") && err.contains("not kept in memory"),
1288 "{err}"
1289 );
1290 assert_eq!(stats.gets.load(Ordering::SeqCst), 1, "permanent: no retry");
1291 assert!(!r.serves_whole_body());
1292 assert!(r.read_many(&[(0, 1), (2000, 1)]).is_err());
1294 }
1295 assert_eq!(FetchPolicy::DEFAULT.whole_body_cap, 256 << 20);
1297 let msg = whole_body_refusal("u", Some(300 << 20), WHOLE_BODY_CAP).to_string();
1298 assert!(
1299 msg.contains("256 MiB") && msg.contains("status 200"),
1300 "{msg}"
1301 );
1302 }
1303
1304 #[test]
1308 fn measures_the_length_without_a_head_content_length() {
1309 let data: Vec<u8> = (0..=255u8).cycle().take(2048).collect();
1310 let (url, stats) = serve_with(data.clone(), ServerMode::HeadWithoutLength, Duration::ZERO);
1311 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1312 assert_eq!(r.len(), 2048);
1313 assert_eq!(r.read_at(100, 8).unwrap(), &data[100..108]);
1314 assert!(!r.serves_whole_body());
1315 assert_eq!(stats.gets.load(Ordering::SeqCst), 2, "probe + read");
1316
1317 let (url, stats) = serve_with(
1318 data.clone(),
1319 ServerMode::HeadWithoutLengthIgnoreRange,
1320 Duration::ZERO,
1321 );
1322 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1323 assert_eq!(r.len(), 2048);
1324 assert!(r.serves_whole_body());
1325 assert_eq!(r.read_at(2000, 48).unwrap(), &data[2000..2048]);
1326 assert_eq!(
1327 stats.gets.load(Ordering::SeqCst),
1328 1,
1329 "the probe was the file"
1330 );
1331 }
1332
1333 #[test]
1335 fn sends_the_extra_headers_on_every_request() {
1336 let data: Vec<u8> = (0..100u8).collect();
1337 let url = serve(data.clone(), ServerMode::RequireHeader);
1338 assert!(HttpRangeReader::open_with(&url, quick()).is_err());
1339 let headers = vec![("x-token".to_string(), "secret".to_string())];
1340 let r = HttpRangeReader::open_with_headers(&url, headers, quick()).unwrap();
1341 assert_eq!(r.len(), 100);
1342 assert_eq!(r.read_many(&[(0, 2), (90, 10)]).unwrap()[1], &data[90..100]);
1343 }
1344
1345 #[test]
1346 fn rejects_a_truncated_range_response() {
1347 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
1351 let (url, stats) = serve_with(data, ServerMode::TruncateBody, Duration::ZERO);
1352 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1353 let err = r.read_at(100, 40).unwrap_err();
1357 let msg = err.to_string();
1358 assert!(
1359 msg.contains("short range response") || msg.contains("closed before"),
1360 "expected a truncation error, got: {err}"
1361 );
1362 assert_eq!(
1363 stats.gets.load(Ordering::SeqCst),
1364 3,
1365 "a truncated body is transient: retried twice"
1366 );
1367 let data: Vec<u8> = (0..100u8).collect();
1369 let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::ZERO);
1370 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1371 let err = r.read_at(90, 20).unwrap_err();
1372 assert!(err.to_string().contains("out of bounds"), "{err}");
1373 assert!(r.read_many(&[(0, 1), (90, 20)]).is_err());
1374 assert_eq!(stats.gets.load(Ordering::SeqCst), 0);
1375 }
1376
1377 #[test]
1378 fn server_errors_are_clean_errors() {
1379 let data: Vec<u8> = (0..100u8).collect();
1380 let (url, stats) = serve_with(data, ServerMode::Error500, Duration::ZERO);
1381 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1382 let err = r.read_at(0, 10).unwrap_err();
1384 assert!(err.to_string().contains("HTTP 500"), "{err}");
1385 assert!(err.to_string().contains("after 3 attempts"), "{err}");
1386 assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
1387 }
1388
1389 #[test]
1390 fn a_missing_file_is_not_retried() {
1391 let (url, stats) = serve_with(vec![0; 100], ServerMode::NotFound, Duration::ZERO);
1392 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1393 let err = r.read_at(0, 10).unwrap_err();
1394 assert!(err.to_string().contains("HTTP 404"), "{err}");
1395 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
1396 }
1397
1398 #[test]
1399 fn a_transient_failure_is_retried_and_recovers() {
1400 let data: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
1401 let (url, stats) = serve_with(data.clone(), ServerMode::Unavailable(2), Duration::ZERO);
1402 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1403 assert_eq!(r.read_at(10, 30).unwrap(), &data[10..40]);
1404 assert_eq!(stats.gets.load(Ordering::SeqCst), 3, "two 503s, then 206");
1405 let (url, _) = serve_with(data, ServerMode::Unavailable(1), Duration::ZERO);
1407 let none = FetchPolicy {
1408 retries: 0,
1409 ..quick()
1410 };
1411 let r = HttpRangeReader::open_with(&url, none).unwrap();
1412 assert!(r.read_at(10, 30).unwrap_err().to_string().contains("503"));
1413 }
1414
1415 #[test]
1416 fn rejects_a_range_answered_for_the_wrong_offset() {
1417 let (url, _) = serve_with(vec![7; 1000], ServerMode::WrongOffset, Duration::ZERO);
1418 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1419 let err = r.read_at(100, 10).unwrap_err();
1420 assert!(
1421 err.to_string().contains("Content-Range bytes 101-109"),
1422 "{err}"
1423 );
1424 }
1425
1426 #[test]
1430 fn rejects_a_wrong_end_a_wrong_total_and_an_overlong_body() {
1431 for (mode, want) in [
1432 (ServerMode::WrongEnd, "Content-Range bytes 100-110"),
1433 (ServerMode::WrongTotal, "changed under the same URL"),
1434 (ServerMode::Overlong, "overlong range response"),
1435 ] {
1436 let (url, _) = serve_with(vec![7; 1000], mode, Duration::ZERO);
1437 let r = HttpRangeReader::open_with(&url, quick()).unwrap();
1438 let err = r.read_at(100, 10).unwrap_err();
1439 assert!(err.to_string().contains(want), "{mode:?}: {err}");
1440 }
1441 let (url, _) = serve_with(vec![7; 100_000], ServerMode::WrongEnd, Duration::ZERO);
1443 let policy = FetchPolicy {
1444 backoff_ms: 1,
1445 ..FetchPolicy::DEFAULT
1446 };
1447 let err = HttpRangeReader::open_with(&url, policy)
1448 .err()
1449 .expect("refused");
1450 assert!(
1451 err.to_string().contains("not the 65536 bytes asked for"),
1452 "{err}"
1453 );
1454 let (url, _) = serve_with(vec![7; 100_000], ServerMode::Overlong, Duration::ZERO);
1455 let err = HttpRangeReader::open_with(&url, policy)
1456 .err()
1457 .expect("refused");
1458 assert!(
1459 err.to_string().contains("longer than its Content-Range"),
1460 "{err}"
1461 );
1462 }
1463
1464 #[test]
1467 fn a_small_file_is_fetched_whole_at_open() {
1468 let data: Vec<u8> = (0..=255u8).cycle().take(300_000).collect();
1469 let (url, stats) = serve_with(data.clone(), ServerMode::HonorRange, Duration::ZERO);
1470 let policy = FetchPolicy {
1471 backoff_ms: 1,
1472 ..FetchPolicy::DEFAULT
1473 };
1474 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1475 assert_eq!(
1476 stats.gets.load(Ordering::SeqCst),
1477 2,
1478 "first read + the rest"
1479 );
1480 assert_eq!(r.read_at(299_990, 10).unwrap(), &data[299_990..]);
1481 assert_eq!(
1482 r.read_many(&[(0, 4), (200_000, 8)]).unwrap()[1],
1483 &data[200_000..200_008]
1484 );
1485 assert_eq!(
1486 stats.gets.load(Ordering::SeqCst),
1487 2,
1488 "nothing after the open"
1489 );
1490 let (url, stats) = serve_with(data.clone(), ServerMode::HonorRange, Duration::ZERO);
1492 let small = FetchPolicy {
1493 whole_max: 100_000,
1494 ..policy
1495 };
1496 let r = HttpRangeReader::open_with(&url, small).unwrap();
1497 assert_eq!(stats.gets.load(Ordering::SeqCst), 1);
1498 assert_eq!(r.read_at(200_000, 8).unwrap(), &data[200_000..200_008]);
1499 assert_eq!(stats.gets.load(Ordering::SeqCst), 2);
1500 }
1501
1502 #[test]
1505 fn read_many_overlaps_requests_up_to_the_bound() {
1506 let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
1507 let (url, stats) = serve_with(
1508 data.clone(),
1509 ServerMode::HonorRange,
1510 Duration::from_millis(40),
1511 );
1512 let policy = FetchPolicy {
1513 concurrency: 4,
1514 coalesce_gap: 0,
1515 ..quick()
1516 };
1517 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1518 let mut ranges: Vec<(u64, u64)> = (0..12).map(|i| (i * 65_536 + 7, 100)).collect();
1520 ranges.reverse();
1521 ranges.insert(3, (5, 0));
1522 let t = std::time::Instant::now();
1523 let got = r.read_many(&ranges).unwrap();
1524 let took = t.elapsed();
1525 assert_eq!(got.len(), ranges.len());
1526 for (&(o, l), b) in ranges.iter().zip(&got) {
1527 assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
1528 }
1529 assert_eq!(stats.gets.load(Ordering::SeqCst), 12);
1530 assert_eq!(
1531 stats.max_in_flight.load(Ordering::SeqCst),
1532 4,
1533 "bounded fan-out"
1534 );
1535 assert!(took < Duration::from_millis(12 * 40), "{took:?}");
1537 }
1538
1539 #[test]
1541 fn concurrency_one_is_sequential() {
1542 let data: Vec<u8> = (0..=255u8).cycle().take(4096).collect();
1543 let (url, stats) = serve_with(data, ServerMode::HonorRange, Duration::from_millis(5));
1544 let policy = FetchPolicy {
1545 concurrency: 1,
1546 coalesce_gap: 0,
1547 ..quick()
1548 };
1549 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1550 assert_eq!(r.concurrency(), 1);
1551 r.read_many(&[(0, 10), (1000, 10), (3000, 10)]).unwrap();
1552 assert_eq!(stats.max_in_flight.load(Ordering::SeqCst), 1);
1553 assert_eq!(stats.gets.load(Ordering::SeqCst), 3);
1554 }
1555
1556 #[test]
1558 fn read_many_coalesces_nearby_ranges() {
1559 let data: Vec<u8> = (0..=255u8).cycle().take(1 << 20).collect();
1560 let (url, stats) = serve_with(data.clone(), ServerMode::HonorRange, Duration::ZERO);
1561 let policy = FetchPolicy {
1562 coalesce_gap: 1024,
1563 ..quick()
1564 };
1565 let r = HttpRangeReader::open_with(&url, policy).unwrap();
1566 let ranges = [(5000, 100), (100, 50), (200, 10), (190, 30), (6000, 1)];
1567 let got = r.read_many(&ranges).unwrap();
1568 for (&(o, l), b) in ranges.iter().zip(&got) {
1569 assert_eq!(b.as_slice(), &data[o as usize..(o + l) as usize]);
1570 }
1571 assert_eq!(stats.gets.load(Ordering::SeqCst), 2);
1573 }
1574
1575 #[test]
1577 fn read_many_fails_the_batch_on_one_bad_range() {
1578 let data: Vec<u8> = (0..100u8).collect();
1579 let url = serve(data, ServerMode::FailAt(50));
1580 let r = HttpRangeReader::open_with(
1581 &url,
1582 FetchPolicy {
1583 coalesce_gap: 0,
1584 ..quick()
1585 },
1586 )
1587 .unwrap();
1588 let err = r.read_many(&[(0, 10), (50, 10), (20, 10)]).unwrap_err();
1589 assert!(
1590 err.to_string().contains("HTTP 404 for bytes 50-59"),
1591 "{err}"
1592 );
1593 assert!(r.read_many(&[(0, 10), (20, 10)]).is_ok());
1594 }
1595
1596 #[test]
1597 fn coalesce_plans_spans_and_placements() {
1598 let (spans, place) = coalesce(&[(10, 5), (0, 0), (18, 4), (100, 1)], 3, 1 << 20).unwrap();
1599 assert_eq!(
1600 spans,
1601 vec![
1602 Span {
1603 offset: 10,
1604 len: 12
1605 },
1606 Span {
1607 offset: 100,
1608 len: 1
1609 }
1610 ]
1611 );
1612 assert_eq!(place, vec![Some((0, 0)), None, Some((0, 8)), Some((1, 0))]);
1613 let (spans, _) = coalesce(&[(0, 10), (10, 10), (20, 10)], 0, 20).unwrap();
1615 assert_eq!(spans.len(), 2);
1616 assert!(coalesce(&[(u64::MAX, 2)], 0, 10).is_err());
1617 }
1618
1619 #[test]
1620 fn parses_content_range() {
1621 let cr = |s, e, t| {
1622 Some(ContentRange {
1623 start: s,
1624 end: e,
1625 total: t,
1626 })
1627 };
1628 assert_eq!(
1629 parse_content_range("bytes 100-199/1000"),
1630 cr(100, 199, Some(1000))
1631 );
1632 assert_eq!(parse_content_range(" bytes 0-0/*"), cr(0, 0, None));
1633 assert_eq!(content_range_total("bytes 0-0/1000"), Some(1000));
1634 assert_eq!(content_range_total("bytes 0-0/*"), None);
1635 for bad in [
1636 "items 1-2/3",
1637 "bytes 5-4/10",
1638 "bytes 0-10/10",
1639 "bytes -1-4/10",
1640 "bytes 1-+4/10",
1641 "bytes 0-4",
1642 "bytes 0-4/x",
1643 "bytes */10",
1644 ] {
1645 assert_eq!(parse_content_range(bad), None, "{bad}");
1646 }
1647 }
1648
1649 #[test]
1650 fn retryable_statuses() {
1651 for c in [408, 429, 500, 502, 503, 504] {
1652 assert!(retryable_status(c), "{c}");
1653 }
1654 for c in [400, 403, 404, 416, 501, 505] {
1655 assert!(!retryable_status(c), "{c}");
1656 }
1657 }
1658}