1use std::io::{self, Read, Seek, SeekFrom};
55use std::sync::OnceLock;
56use std::time::Duration;
57
58use oxideav_core::BytesSource;
59use oxideav_core::RuntimeContext;
60use oxideav_core::{Error, Result};
61use oxideav_source::SourceRegistry;
62use ureq::Agent;
63
64pub fn register(ctx: &mut RuntimeContext) {
69 register_source(&mut ctx.sources);
70}
71
72oxideav_core::register!("http", register);
73
74pub fn register_source(registry: &mut SourceRegistry) {
81 registry.register_bytes("http", open_http);
82 registry.register_bytes("https", open_http);
83}
84
85pub fn open_http(uri: &str) -> Result<Box<dyn BytesSource>> {
87 let src = HttpSource::open(uri)?;
88 Ok(Box::new(src))
89}
90
91#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
102pub enum RedirectAuthPolicy {
103 #[default]
105 Never,
106 SameHost,
110}
111
112#[derive(Debug, Clone)]
124pub struct HttpConfig {
125 max_redirects: u32,
126 max_redirects_will_error: bool,
127 redirect_auth_policy: RedirectAuthPolicy,
128 user_agent: Option<String>,
129 https_only: bool,
130 timeout_global: Option<Duration>,
131 timeout_connect: Option<Duration>,
132}
133
134impl Default for HttpConfig {
135 fn default() -> Self {
136 Self {
141 max_redirects: 10,
142 max_redirects_will_error: true,
143 redirect_auth_policy: RedirectAuthPolicy::Never,
144 user_agent: None,
145 https_only: false,
146 timeout_global: None,
147 timeout_connect: None,
148 }
149 }
150}
151
152impl HttpConfig {
153 pub fn builder() -> HttpConfigBuilder {
155 HttpConfigBuilder {
156 inner: Self::default(),
157 }
158 }
159
160 pub fn max_redirects(&self) -> u32 {
163 self.max_redirects
164 }
165
166 pub fn max_redirects_will_error(&self) -> bool {
169 self.max_redirects_will_error
170 }
171
172 pub fn redirect_auth_policy(&self) -> RedirectAuthPolicy {
174 self.redirect_auth_policy
175 }
176
177 pub fn user_agent(&self) -> Option<&str> {
179 self.user_agent.as_deref()
180 }
181
182 pub fn https_only(&self) -> bool {
185 self.https_only
186 }
187
188 pub fn timeout_global(&self) -> Option<Duration> {
190 self.timeout_global
191 }
192
193 pub fn timeout_connect(&self) -> Option<Duration> {
195 self.timeout_connect
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct HttpConfigBuilder {
202 inner: HttpConfig,
203}
204
205impl HttpConfigBuilder {
206 pub fn max_redirects(mut self, n: u32) -> Self {
208 self.inner.max_redirects = n;
209 self
210 }
211
212 pub fn max_redirects_will_error(mut self, v: bool) -> Self {
215 self.inner.max_redirects_will_error = v;
216 self
217 }
218
219 pub fn redirect_auth_policy(mut self, p: RedirectAuthPolicy) -> Self {
222 self.inner.redirect_auth_policy = p;
223 self
224 }
225
226 pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
229 self.inner.user_agent = Some(ua.into());
230 self
231 }
232
233 pub fn https_only(mut self, v: bool) -> Self {
236 self.inner.https_only = v;
237 self
238 }
239
240 pub fn timeout_global(mut self, d: Option<Duration>) -> Self {
242 self.inner.timeout_global = d;
243 self
244 }
245
246 pub fn timeout_connect(mut self, d: Option<Duration>) -> Self {
248 self.inner.timeout_connect = d;
249 self
250 }
251
252 pub fn build(self) -> HttpConfig {
254 self.inner
255 }
256}
257
258fn agent_from(cfg: &HttpConfig) -> Agent {
259 let mut b = Agent::config_builder()
260 .max_redirects(cfg.max_redirects)
261 .max_redirects_will_error(cfg.max_redirects_will_error)
262 .https_only(cfg.https_only)
263 .redirect_auth_headers(match cfg.redirect_auth_policy {
264 RedirectAuthPolicy::Never => ureq::config::RedirectAuthHeaders::Never,
265 RedirectAuthPolicy::SameHost => ureq::config::RedirectAuthHeaders::SameHost,
266 })
267 .http_status_as_error(false);
273 if let Some(ua) = cfg.user_agent.as_deref() {
274 b = b.user_agent(ua);
275 }
276 b = b
277 .timeout_global(cfg.timeout_global)
278 .timeout_connect(cfg.timeout_connect);
279 b.build().new_agent()
280}
281
282static DEFAULT_CONFIG: OnceLock<HttpConfig> = OnceLock::new();
287static DEFAULT_AGENT: OnceLock<Agent> = OnceLock::new();
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub struct ConfigAlreadyInstalled;
294
295impl std::fmt::Display for ConfigAlreadyInstalled {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.write_str("oxideav-http default config has already been installed")
298 }
299}
300
301impl std::error::Error for ConfigAlreadyInstalled {}
302
303pub fn install_default_config(cfg: HttpConfig) -> std::result::Result<(), ConfigAlreadyInstalled> {
312 if DEFAULT_AGENT.get().is_some() {
313 return Err(ConfigAlreadyInstalled);
314 }
315 DEFAULT_CONFIG.set(cfg).map_err(|_| ConfigAlreadyInstalled)
316}
317
318fn agent() -> &'static Agent {
319 DEFAULT_AGENT.get_or_init(|| {
320 let cfg = DEFAULT_CONFIG.get().cloned().unwrap_or_default();
321 agent_from(&cfg)
322 })
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
339enum StrongValidator {
340 Etag(String),
343 LastModified(String),
346}
347
348impl StrongValidator {
349 fn as_if_range(&self) -> &str {
351 match self {
352 StrongValidator::Etag(s) => s,
353 StrongValidator::LastModified(s) => s,
354 }
355 }
356}
357
358pub struct HttpSource {
360 uri: String,
361 total_len: u64,
362 pos: u64,
363 agent: Option<Agent>,
365 validator: Option<StrongValidator>,
370 body: Option<Box<dyn Read + Send>>,
372}
373
374impl HttpSource {
375 pub fn open(uri: &str) -> Result<Self> {
381 Self::open_impl(uri, None)
382 }
383
384 pub fn open_with_config(uri: &str, cfg: &HttpConfig) -> Result<Self> {
390 Self::open_impl(uri, Some(agent_from(cfg)))
391 }
392
393 fn open_impl(uri: &str, scoped: Option<Agent>) -> Result<Self> {
394 let head_agent: &Agent = scoped.as_ref().unwrap_or_else(|| agent());
395 let head = head_agent
396 .head(uri)
397 .call()
398 .map_err(|e| Error::other(format!("HTTP HEAD {uri}: {e}")))?;
399
400 let status = head.status();
401 if !status.is_success() {
402 return Err(Error::other(format!("HTTP HEAD {uri}: status {status}")));
403 }
404 let headers = head.headers();
405 let total_len = headers
406 .get("content-length")
407 .and_then(|v| v.to_str().ok())
408 .and_then(|s| s.parse::<u64>().ok())
409 .ok_or_else(|| {
410 Error::Unsupported(format!("HTTP HEAD {uri}: missing Content-Length"))
411 })?;
412 let accept_ranges = headers
413 .get("accept-ranges")
414 .and_then(|v| v.to_str().ok())
415 .unwrap_or("");
416 if !accept_ranges.eq_ignore_ascii_case("bytes") {
417 return Err(Error::Unsupported(format!(
418 "HTTP HEAD {uri}: server does not advertise byte ranges (Accept-Ranges: '{accept_ranges}')"
419 )));
420 }
421 let etag_raw = headers
429 .get("etag")
430 .and_then(|v| v.to_str().ok())
431 .map(str::trim);
432 let last_modified = headers
433 .get("last-modified")
434 .and_then(|v| v.to_str().ok())
435 .map(str::trim);
436 let date_hdr = headers
437 .get("date")
438 .and_then(|v| v.to_str().ok())
439 .map(str::trim);
440 let validator = derive_strong_validator(etag_raw, last_modified, date_hdr);
441 Ok(Self {
442 uri: uri.to_owned(),
443 total_len,
444 pos: 0,
445 agent: scoped,
446 validator,
447 body: None,
448 })
449 }
450
451 pub fn len(&self) -> u64 {
452 self.total_len
453 }
454
455 pub fn is_empty(&self) -> bool {
456 self.total_len == 0
457 }
458
459 fn agent_ref(&self) -> &Agent {
460 self.agent.as_ref().unwrap_or_else(|| agent())
461 }
462
463 fn issue_range(&mut self) -> io::Result<()> {
464 if self.pos >= self.total_len {
465 self.body = None;
466 return Ok(());
467 }
468 let range = format!("bytes={}-", self.pos);
469 let if_range = self.validator.as_ref().map(|v| v.as_if_range().to_owned());
475 let sent_if_range = if_range.is_some();
476 let mut req = self.agent_ref().get(&self.uri).header("Range", &range);
477 if let Some(ref v) = if_range {
478 req = req.header("If-Range", v.as_str());
479 }
480 let resp = req
481 .call()
482 .map_err(|e| io::Error::other(format!("HTTP GET {} {}: {e}", self.uri, range)))?;
483 let status = resp.status();
484 if status == 416 {
492 let cr_raw = resp
493 .headers()
494 .get("content-range")
495 .and_then(|v| v.to_str().ok())
496 .map(str::to_owned);
497 if let Some(cr) = cr_raw.as_deref() {
498 match parse_byte_unsatisfied_range(cr) {
499 Ok(complete) => {
500 return Err(io::Error::other(format!(
501 "HTTP 416 {} {}: server reports complete-length {complete} (HEAD observed {}, requested pos {})",
502 self.uri, range, self.total_len, self.pos
503 )));
504 }
505 Err(e) => {
506 return Err(io::Error::other(format!(
507 "HTTP 416 {} {}: invalid Content-Range '{cr}': {e}",
508 self.uri, range
509 )));
510 }
511 }
512 }
513 return Err(io::Error::other(format!(
517 "HTTP 416 {} {}: server rejected range (no Content-Range body)",
518 self.uri, range
519 )));
520 }
521 if !(status == 206 || status == 200) {
522 return Err(io::Error::other(format!(
523 "HTTP GET {} {}: status {status}",
524 self.uri, range
525 )));
526 }
527 if status == 200 && sent_if_range {
534 return Err(io::Error::other(format!(
535 "HTTP 200 {} {}: If-Range validator did not match — \
536 representation changed since HEAD (origin/cache mutation)",
537 self.uri, range
538 )));
539 }
540 let pos = self.pos;
541 let total_len = self.total_len;
542 let get_content_length = resp
551 .headers()
552 .get("content-length")
553 .and_then(|v| v.to_str().ok())
554 .and_then(|s| s.parse::<u64>().ok());
555 let skip_prefix = if status == 200 { pos } else { 0 };
563 if status == 200 {
564 if let Some(cl) = get_content_length {
565 if cl != total_len {
566 return Err(io::Error::other(format!(
567 "HTTP 200 {} {}: Content-Length {cl} != HEAD-observed total {total_len} \
568 (RFC 9110 §8.6 — resource resized between HEAD and GET)",
569 self.uri, range
570 )));
571 }
572 }
573 }
574 if status == 206 {
575 let cr_raw = resp
582 .headers()
583 .get("content-range")
584 .ok_or_else(|| {
585 io::Error::other(format!(
586 "HTTP 206 {} {}: missing Content-Range",
587 self.uri, range
588 ))
589 })?
590 .to_str()
591 .map_err(|_| {
592 io::Error::other(format!(
593 "HTTP 206 {} {}: non-ASCII Content-Range",
594 self.uri, range
595 ))
596 })?
597 .to_owned();
598 let parsed = parse_byte_content_range(&cr_raw).map_err(|e| {
599 io::Error::other(format!(
600 "HTTP 206 {} {}: invalid Content-Range '{cr_raw}': {e}",
601 self.uri, range
602 ))
603 })?;
604 if parsed.first != pos {
608 return Err(io::Error::other(format!(
609 "HTTP 206 {} {}: Content-Range first-byte-pos {} != requested pos {}",
610 self.uri, range, parsed.first, pos
611 )));
612 }
613 if let Some(complete) = parsed.complete {
617 if complete != total_len {
618 return Err(io::Error::other(format!(
619 "HTTP 206 {} {}: Content-Range complete-length {complete} != known total {total_len}",
620 self.uri, range
621 )));
622 }
623 }
624 if parsed.last >= total_len {
627 return Err(io::Error::other(format!(
628 "HTTP 206 {} {}: Content-Range last-byte-pos {} >= total {}",
629 self.uri, range, parsed.last, total_len
630 )));
631 }
632 if let Some(cl) = get_content_length {
642 let expected = parsed.last - parsed.first + 1;
643 if cl != expected {
644 return Err(io::Error::other(format!(
645 "HTTP 206 {} {}: Content-Length {cl} != Content-Range span {expected} \
646 (RFC 9110 §8.6)",
647 self.uri, range
648 )));
649 }
650 }
651 }
652 let mut reader: Box<dyn Read + Send> = Box::new(resp.into_body().into_reader());
655 if skip_prefix > 0 {
656 let mut remaining = skip_prefix;
659 let mut buf = [0u8; 8 * 1024];
660 while remaining > 0 {
661 let want = remaining.min(buf.len() as u64) as usize;
662 let n = reader.read(&mut buf[..want])?;
663 if n == 0 {
664 return Err(io::Error::new(
665 io::ErrorKind::UnexpectedEof,
666 format!(
667 "HTTP 200 {}: EOF after draining {} of {skip_prefix} prefix bytes",
668 self.uri,
669 skip_prefix - remaining
670 ),
671 ));
672 }
673 remaining -= n as u64;
674 }
675 }
676 self.body = Some(reader);
677 Ok(())
678 }
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Eq)]
688struct ByteContentRange {
689 first: u64,
690 last: u64,
691 complete: Option<u64>,
693}
694
695fn parse_byte_content_range(s: &str) -> std::result::Result<ByteContentRange, &'static str> {
711 let s = s.trim();
712 let (unit, rest) = s.split_once(' ').ok_or("missing SP after range unit")?;
714 if !unit.eq_ignore_ascii_case("bytes") {
715 return Err("range unit is not 'bytes'");
716 }
717 let rest = rest.trim_start();
718 if rest.starts_with('*') {
721 return Err("unsatisfied-range ('*/N') not valid on 206");
722 }
723 let (range_part, complete_part) = rest
724 .split_once('/')
725 .ok_or("missing '/' before complete-length")?;
726 let (first_s, last_s) = range_part
727 .split_once('-')
728 .ok_or("missing '-' between first-byte-pos and last-byte-pos")?;
729 let first: u64 = first_s.parse().map_err(|_| "first-byte-pos is not a u64")?;
730 let last: u64 = last_s.parse().map_err(|_| "last-byte-pos is not a u64")?;
731 if last < first {
732 return Err("last-byte-pos < first-byte-pos");
733 }
734 let complete = if complete_part == "*" {
735 None
736 } else {
737 let c: u64 = complete_part
738 .parse()
739 .map_err(|_| "complete-length is not a u64 or '*'")?;
740 if c <= last {
741 return Err("complete-length <= last-byte-pos");
742 }
743 Some(c)
744 };
745 Ok(ByteContentRange {
746 first,
747 last,
748 complete,
749 })
750}
751
752fn parse_byte_unsatisfied_range(s: &str) -> std::result::Result<u64, &'static str> {
766 let s = s.trim();
767 let (unit, rest) = s.split_once(' ').ok_or("missing SP after range unit")?;
768 if !unit.eq_ignore_ascii_case("bytes") {
769 return Err("range unit is not 'bytes'");
770 }
771 let rest = rest.trim_start();
772 let complete_part = rest
773 .strip_prefix("*/")
774 .ok_or("not an unsatisfied-range ('*/N' expected)")?;
775 let complete: u64 = complete_part
776 .parse()
777 .map_err(|_| "complete-length is not a u64")?;
778 Ok(complete)
779}
780
781fn parse_entity_tag(s: &str) -> Option<(bool, String)> {
797 let s = s.trim();
798 let (is_weak, body) = if let Some(rest) = s.strip_prefix("W/") {
800 (true, rest)
801 } else {
802 (false, s)
803 };
804 let body = body.strip_prefix('"')?;
805 let body = body.strip_suffix('"')?;
806 for b in body.bytes() {
810 let ok = b == 0x21 || (0x23..=0x7E).contains(&b) || b >= 0x80;
811 if !ok {
812 return None;
813 }
814 }
815 Some((is_weak, s.to_owned()))
816}
817
818fn parse_imf_fixdate(s: &str) -> Option<(i32, u8, u8, u8, u8, u8)> {
827 let s = s.trim();
829 let bytes = s.as_bytes();
830 if bytes.len() < 29 {
831 return None;
832 }
833 if bytes[3] != b',' || bytes[4] != b' ' {
835 return None;
836 }
837 let day: u8 = s.get(5..7)?.parse().ok()?;
838 if bytes[7] != b' ' {
839 return None;
840 }
841 let mon = match &s[8..11] {
842 "Jan" => 1,
843 "Feb" => 2,
844 "Mar" => 3,
845 "Apr" => 4,
846 "May" => 5,
847 "Jun" => 6,
848 "Jul" => 7,
849 "Aug" => 8,
850 "Sep" => 9,
851 "Oct" => 10,
852 "Nov" => 11,
853 "Dec" => 12,
854 _ => return None,
855 };
856 if bytes[11] != b' ' {
857 return None;
858 }
859 let year: i32 = s.get(12..16)?.parse().ok()?;
860 if bytes[16] != b' ' {
861 return None;
862 }
863 let hour: u8 = s.get(17..19)?.parse().ok()?;
864 if bytes[19] != b':' {
865 return None;
866 }
867 let minute: u8 = s.get(20..22)?.parse().ok()?;
868 if bytes[22] != b':' {
869 return None;
870 }
871 let second: u8 = s.get(23..25)?.parse().ok()?;
872 if &s[25..] != " GMT" {
873 return None;
874 }
875 Some((year, mon, day, hour, minute, second))
876}
877
878fn imf_seconds(d: (i32, u8, u8, u8, u8, u8)) -> i64 {
885 let (y, mo, da, h, mi, s) = d;
886 const CUM: [u16; 13] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
890 let day_of_year = CUM[(mo - 1) as usize] as i64 + (da as i64 - 1);
891 let day_index = (y as i64 - 2000) * 365 + day_of_year;
892 day_index * 86_400 + (h as i64) * 3_600 + (mi as i64) * 60 + s as i64
893}
894
895fn derive_strong_validator(
906 etag: Option<&str>,
907 last_modified: Option<&str>,
908 date: Option<&str>,
909) -> Option<StrongValidator> {
910 if let Some(raw) = etag {
911 if let Some((is_weak, wire)) = parse_entity_tag(raw) {
912 if !is_weak {
913 return Some(StrongValidator::Etag(wire));
914 }
915 }
916 }
917 if let (Some(lm), Some(dt)) = (last_modified, date) {
918 if let (Some(lm_p), Some(dt_p)) = (parse_imf_fixdate(lm), parse_imf_fixdate(dt)) {
919 if imf_seconds(dt_p) > imf_seconds(lm_p) {
923 return Some(StrongValidator::LastModified(lm.to_owned()));
924 }
925 }
926 }
927 None
928}
929
930impl Read for HttpSource {
931 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
932 if out.is_empty() {
933 return Ok(0);
934 }
935 if self.pos >= self.total_len {
936 return Ok(0);
937 }
938 loop {
939 if self.body.is_none() {
940 self.issue_range()?;
941 }
942 let body = self.body.as_mut().expect("body just issued");
943 let n = body.read(out)?;
944 if n > 0 {
945 self.pos += n as u64;
946 return Ok(n);
947 }
948 self.body = None;
949 if self.pos >= self.total_len {
950 return Ok(0);
951 }
952 }
953 }
954}
955
956impl Seek for HttpSource {
957 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
958 let new_pos: u64 = match from {
959 SeekFrom::Start(n) => n,
960 SeekFrom::Current(d) => add_signed(self.pos, d)?,
961 SeekFrom::End(d) => add_signed(self.total_len, d)?,
962 };
963 if new_pos > self.total_len {
964 return Err(io::Error::new(io::ErrorKind::InvalidInput, "seek past end"));
965 }
966 if new_pos != self.pos {
967 self.body = None;
968 self.pos = new_pos;
969 }
970 Ok(new_pos)
971 }
972}
973
974fn add_signed(base: u64, delta: i64) -> io::Result<u64> {
975 if delta >= 0 {
976 base.checked_add(delta as u64)
977 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow"))
978 } else {
979 base.checked_sub(delta.unsigned_abs())
980 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek before start"))
981 }
982}
983
984#[cfg(feature = "fuzz")]
993#[doc(hidden)]
994pub mod __fuzz {
995 pub fn parse_byte_content_range(s: &str) -> bool {
997 super::parse_byte_content_range(s).is_ok()
998 }
999 pub fn parse_byte_unsatisfied_range(s: &str) -> bool {
1001 super::parse_byte_unsatisfied_range(s).is_ok()
1002 }
1003 pub fn parse_entity_tag(s: &str) -> bool {
1005 super::parse_entity_tag(s).is_some()
1006 }
1007 pub fn parse_imf_fixdate(s: &str) -> bool {
1009 super::parse_imf_fixdate(s).is_some()
1010 }
1011 pub fn derive_strong_validator(etag: Option<&str>, lm: Option<&str>, date: Option<&str>) {
1016 let _ = super::derive_strong_validator(etag, lm, date);
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023 use std::collections::BTreeSet;
1024
1025 #[test]
1026 fn register_via_runtime_context_installs_http_and_https_schemes() {
1027 let mut ctx = RuntimeContext::new();
1028 register(&mut ctx);
1029 let schemes: BTreeSet<&str> = ctx.sources.schemes().collect();
1030 assert!(
1031 schemes.contains("http"),
1032 "register did not install http; got {schemes:?}"
1033 );
1034 assert!(
1035 schemes.contains("https"),
1036 "register did not install https; got {schemes:?}"
1037 );
1038 }
1039
1040 #[test]
1041 fn register_source_installs_http_and_https_schemes_on_bare_registry() {
1042 let mut reg = SourceRegistry::new();
1043 register_source(&mut reg);
1044 let schemes: BTreeSet<&str> = reg.schemes().collect();
1045 assert!(schemes.contains("http"));
1046 assert!(schemes.contains("https"));
1047 }
1048
1049 #[test]
1050 fn http_config_default_matches_documented_surface() {
1051 let c = HttpConfig::default();
1052 assert_eq!(c.max_redirects(), 10);
1053 assert!(c.max_redirects_will_error());
1054 assert_eq!(c.redirect_auth_policy(), RedirectAuthPolicy::Never);
1055 assert_eq!(c.user_agent(), None);
1056 assert!(!c.https_only());
1057 assert_eq!(c.timeout_global(), None);
1058 assert_eq!(c.timeout_connect(), None);
1059 }
1060
1061 #[test]
1062 fn http_config_builder_threads_values_through() {
1063 let c = HttpConfig::builder()
1064 .max_redirects(3)
1065 .max_redirects_will_error(false)
1066 .redirect_auth_policy(RedirectAuthPolicy::SameHost)
1067 .user_agent("oxideav-test/0.0")
1068 .https_only(true)
1069 .timeout_global(Some(Duration::from_secs(30)))
1070 .timeout_connect(Some(Duration::from_secs(5)))
1071 .build();
1072 assert_eq!(c.max_redirects(), 3);
1073 assert!(!c.max_redirects_will_error());
1074 assert_eq!(c.redirect_auth_policy(), RedirectAuthPolicy::SameHost);
1075 assert_eq!(c.user_agent(), Some("oxideav-test/0.0"));
1076 assert!(c.https_only());
1077 assert_eq!(c.timeout_global(), Some(Duration::from_secs(30)));
1078 assert_eq!(c.timeout_connect(), Some(Duration::from_secs(5)));
1079 }
1080
1081 #[test]
1082 fn agent_from_config_succeeds_for_every_redirect_policy() {
1083 for p in [RedirectAuthPolicy::Never, RedirectAuthPolicy::SameHost] {
1086 let c = HttpConfig::builder().redirect_auth_policy(p).build();
1087 let _ = agent_from(&c);
1088 }
1089 }
1090
1091 #[test]
1092 fn http_config_clone_is_cheap_and_independent() {
1093 let a = HttpConfig::builder()
1094 .user_agent("alpha/1.0")
1095 .max_redirects(7)
1096 .build();
1097 let b = a.clone();
1098 assert_eq!(a.user_agent(), b.user_agent());
1099 assert_eq!(a.max_redirects(), b.max_redirects());
1100 }
1101
1102 #[test]
1103 fn install_default_config_is_one_shot() {
1104 let first = install_default_config(HttpConfig::default());
1112 let _ = agent();
1115 let second = install_default_config(HttpConfig::default());
1116 assert!(
1117 first.is_err() || second.is_err(),
1118 "expected at least one install to be rejected once the agent is built (first={first:?}, second={second:?})"
1119 );
1120 assert_eq!(second, Err(ConfigAlreadyInstalled));
1121 }
1122
1123 #[test]
1124 fn config_already_installed_implements_error_trait() {
1125 let e: &dyn std::error::Error = &ConfigAlreadyInstalled;
1126 assert!(e.to_string().contains("already"));
1127 }
1128
1129 #[test]
1132 fn content_range_parses_canonical_form() {
1133 let r = parse_byte_content_range("bytes 42-1233/1234").unwrap();
1134 assert_eq!(
1135 r,
1136 ByteContentRange {
1137 first: 42,
1138 last: 1233,
1139 complete: Some(1234),
1140 }
1141 );
1142 }
1143
1144 #[test]
1145 fn content_range_accepts_star_complete_length() {
1146 let r = parse_byte_content_range("bytes 42-1233/*").unwrap();
1150 assert_eq!(r.first, 42);
1151 assert_eq!(r.last, 1233);
1152 assert_eq!(r.complete, None);
1153 }
1154
1155 #[test]
1156 fn content_range_accepts_full_resource_first_last() {
1157 let r = parse_byte_content_range("bytes 0-0/1").unwrap();
1158 assert_eq!(r.first, 0);
1159 assert_eq!(r.last, 0);
1160 assert_eq!(r.complete, Some(1));
1161 }
1162
1163 #[test]
1164 fn content_range_unit_is_case_insensitive() {
1165 assert!(parse_byte_content_range("BYTES 0-9/10").is_ok());
1167 assert!(parse_byte_content_range("Bytes 0-9/10").is_ok());
1168 }
1169
1170 #[test]
1171 fn content_range_rejects_unknown_unit() {
1172 assert!(parse_byte_content_range("frames 0-9/100").is_err());
1177 }
1178
1179 #[test]
1180 fn content_range_rejects_last_lt_first() {
1181 assert!(parse_byte_content_range("bytes 100-50/200").is_err());
1183 }
1184
1185 #[test]
1186 fn content_range_rejects_complete_le_last() {
1187 assert!(parse_byte_content_range("bytes 0-100/100").is_err());
1189 assert!(parse_byte_content_range("bytes 0-100/50").is_err());
1190 }
1191
1192 #[test]
1193 fn content_range_rejects_unsatisfied_payload_on_206() {
1194 assert!(parse_byte_content_range("bytes */1234").is_err());
1197 }
1198
1199 #[test]
1200 fn unsatisfied_range_parses_canonical_form() {
1201 let n = parse_byte_unsatisfied_range("bytes */1234").unwrap();
1203 assert_eq!(n, 1234);
1204 }
1205
1206 #[test]
1207 fn unsatisfied_range_unit_is_case_insensitive() {
1208 assert_eq!(
1209 parse_byte_unsatisfied_range("BYTES */47022").unwrap(),
1210 47022
1211 );
1212 assert_eq!(
1213 parse_byte_unsatisfied_range("Bytes */47022").unwrap(),
1214 47022
1215 );
1216 }
1217
1218 #[test]
1219 fn unsatisfied_range_rejects_unknown_unit() {
1220 assert!(parse_byte_unsatisfied_range("frames */1234").is_err());
1221 }
1222
1223 #[test]
1224 fn unsatisfied_range_rejects_range_resp_form() {
1225 assert!(parse_byte_unsatisfied_range("bytes 0-9/10").is_err());
1228 assert!(parse_byte_unsatisfied_range("bytes 42-1233/1234").is_err());
1229 }
1230
1231 #[test]
1232 fn unsatisfied_range_rejects_malformed() {
1233 for bad in [
1234 "",
1235 "bytes",
1236 "bytes */", "bytes */x", "bytes /1234",
1239 "*/1234", ] {
1241 assert!(
1242 parse_byte_unsatisfied_range(bad).is_err(),
1243 "expected reject for {bad:?}"
1244 );
1245 }
1246 }
1247
1248 #[test]
1249 fn content_range_rejects_malformed_strings() {
1250 for bad in [
1251 "",
1252 "bytes",
1253 "bytes 0-9", "bytes 09/10", "bytes a-9/10", "bytes 0-b/10", "bytes 0-9/x", "bytes 18446744073709551616-0/10", "0-9/10", ] {
1261 assert!(
1262 parse_byte_content_range(bad).is_err(),
1263 "expected reject for {bad:?}"
1264 );
1265 }
1266 }
1267
1268 use std::io::Write as _;
1277 use std::net::TcpListener;
1278 use std::sync::mpsc;
1279 use std::thread;
1280
1281 fn spawn_server(
1287 head_resp: &'static [u8],
1288 get_resp: &'static [u8],
1289 ) -> (String, mpsc::Receiver<()>) {
1290 let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
1291 let port = listener.local_addr().unwrap().port();
1292 let (tx, rx) = mpsc::channel::<()>();
1293 thread::spawn(move || {
1294 for _ in 0..4 {
1297 let Ok((mut stream, _)) = listener.accept() else {
1298 break;
1299 };
1300 let mut buf = [0u8; 4096];
1301 use std::io::Read as _;
1302 let n = stream.read(&mut buf).unwrap_or(0);
1303 if n == 0 {
1304 continue;
1305 }
1306 let req = &buf[..n];
1307 let resp = if req.starts_with(b"HEAD ") {
1308 head_resp
1309 } else {
1310 get_resp
1311 };
1312 let _ = stream.write_all(resp);
1313 let _ = stream.flush();
1314 }
1315 let _ = tx.send(());
1316 });
1317 (format!("http://127.0.0.1:{port}/x"), rx)
1318 }
1319
1320 const HEAD_10B_BYTES: &[u8] = b"HTTP/1.1 200 OK\r\n\
1321 Content-Length: 10\r\n\
1322 Accept-Ranges: bytes\r\n\
1323 Connection: close\r\n\
1324 \r\n";
1325
1326 fn make_get_206(content_range: &str, body: &[u8]) -> Vec<u8> {
1327 let mut v = format!(
1328 "HTTP/1.1 206 Partial Content\r\n\
1329 Content-Length: {}\r\n\
1330 Content-Range: {content_range}\r\n\
1331 Connection: close\r\n\
1332 \r\n",
1333 body.len()
1334 )
1335 .into_bytes();
1336 v.extend_from_slice(body);
1337 v
1338 }
1339
1340 #[test]
1341 fn local_server_canonical_206_succeeds() {
1342 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1345 Content-Length: 10\r\n\
1346 Content-Range: bytes 0-9/10\r\n\
1347 Connection: close\r\n\
1348 \r\n\
1349 0123456789";
1350 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1351 let mut src = HttpSource::open(&uri).expect("open");
1352 let mut buf = [0u8; 10];
1353 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1354 assert_eq!(&buf, b"0123456789");
1355 }
1356
1357 #[test]
1358 fn local_server_206_without_content_range_is_rejected() {
1359 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1360 Content-Length: 10\r\n\
1361 Connection: close\r\n\
1362 \r\n\
1363 0123456789";
1364 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1365 let mut src = HttpSource::open(&uri).expect("open");
1366 let mut buf = [0u8; 10];
1367 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1368 assert!(
1369 err.to_string().contains("missing Content-Range"),
1370 "wrong error: {err}"
1371 );
1372 }
1373
1374 #[test]
1375 fn local_server_206_with_wrong_first_pos_is_rejected() {
1376 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1379 Content-Length: 7\r\n\
1380 Content-Range: bytes 3-9/10\r\n\
1381 Connection: close\r\n\
1382 \r\n\
1383 3456789";
1384 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1385 let mut src = HttpSource::open(&uri).expect("open");
1386 let mut buf = [0u8; 10];
1387 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1388 assert!(
1389 err.to_string().contains("first-byte-pos"),
1390 "wrong error: {err}"
1391 );
1392 }
1393
1394 #[test]
1395 fn local_server_206_with_resource_resize_is_rejected() {
1396 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1399 Content-Length: 10\r\n\
1400 Content-Range: bytes 0-9/20\r\n\
1401 Connection: close\r\n\
1402 \r\n\
1403 0123456789";
1404 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1405 let mut src = HttpSource::open(&uri).expect("open");
1406 let mut buf = [0u8; 10];
1407 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1408 assert!(
1409 err.to_string().contains("complete-length"),
1410 "wrong error: {err}"
1411 );
1412 }
1413
1414 #[test]
1415 fn local_server_206_with_star_complete_is_accepted() {
1416 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1419 Content-Length: 10\r\n\
1420 Content-Range: bytes 0-9/*\r\n\
1421 Connection: close\r\n\
1422 \r\n\
1423 0123456789";
1424 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1425 let mut src = HttpSource::open(&uri).expect("open");
1426 let mut buf = [0u8; 10];
1427 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1428 assert_eq!(&buf, b"0123456789");
1429 }
1430
1431 #[test]
1432 fn local_server_200_after_seek_drops_prefix() {
1433 static GET: &[u8] = b"HTTP/1.1 200 OK\r\n\
1437 Content-Length: 10\r\n\
1438 Connection: close\r\n\
1439 \r\n\
1440 0123456789";
1441 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1442 let mut src = HttpSource::open(&uri).expect("open");
1443 std::io::Seek::seek(&mut src, SeekFrom::Start(4)).unwrap();
1444 let mut buf = [0u8; 6];
1445 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1446 assert_eq!(&buf, b"456789");
1447 }
1448
1449 #[test]
1450 fn local_server_416_with_unsatisfied_range_surfaces_complete_length() {
1451 static GET: &[u8] = b"HTTP/1.1 416 Range Not Satisfiable\r\n\
1458 Content-Length: 0\r\n\
1459 Content-Range: bytes */5\r\n\
1460 Connection: close\r\n\
1461 \r\n";
1462 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1463 let mut src = HttpSource::open(&uri).expect("open");
1464 std::io::Seek::seek(&mut src, SeekFrom::Start(8)).unwrap();
1467 let mut buf = [0u8; 1];
1468 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1469 let msg = err.to_string();
1470 assert!(msg.contains("416"), "wrong error (no 416): {msg}");
1471 assert!(
1472 msg.contains("complete-length 5"),
1473 "wrong error (no 'complete-length 5'): {msg}"
1474 );
1475 assert!(
1476 msg.contains("HEAD observed 10"),
1477 "wrong error (no 'HEAD observed 10'): {msg}"
1478 );
1479 }
1480
1481 #[test]
1482 fn local_server_416_without_content_range_still_errors_cleanly() {
1483 static GET: &[u8] = b"HTTP/1.1 416 Range Not Satisfiable\r\n\
1487 Content-Length: 0\r\n\
1488 Connection: close\r\n\
1489 \r\n";
1490 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1491 let mut src = HttpSource::open(&uri).expect("open");
1492 std::io::Seek::seek(&mut src, SeekFrom::Start(8)).unwrap();
1493 let mut buf = [0u8; 1];
1494 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1495 let msg = err.to_string();
1496 assert!(msg.contains("416"), "wrong error (no 416): {msg}");
1497 assert!(
1498 msg.contains("no Content-Range"),
1499 "wrong error (no 'no Content-Range'): {msg}"
1500 );
1501 }
1502
1503 #[test]
1504 fn local_server_416_with_invalid_content_range_reports_parse_error() {
1505 static GET: &[u8] = b"HTTP/1.1 416 Range Not Satisfiable\r\n\
1509 Content-Length: 0\r\n\
1510 Content-Range: bytes 0-9/10\r\n\
1511 Connection: close\r\n\
1512 \r\n";
1513 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1517 let mut src = HttpSource::open(&uri).expect("open");
1518 std::io::Seek::seek(&mut src, SeekFrom::Start(8)).unwrap();
1519 let mut buf = [0u8; 1];
1520 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1521 let msg = err.to_string();
1522 assert!(msg.contains("416"), "wrong error (no 416): {msg}");
1523 assert!(
1524 msg.contains("invalid Content-Range"),
1525 "wrong error (no 'invalid Content-Range'): {msg}"
1526 );
1527 }
1528
1529 #[allow(dead_code)]
1532 fn _keep_helper() {
1533 let _ = make_get_206("bytes 0-9/10", b"0123456789");
1534 }
1535
1536 #[test]
1539 fn entity_tag_parses_strong_tag() {
1540 let (weak, wire) = parse_entity_tag("\"xyzzy\"").unwrap();
1541 assert!(!weak);
1542 assert_eq!(wire, "\"xyzzy\"");
1543 }
1544
1545 #[test]
1546 fn entity_tag_parses_weak_tag() {
1547 let (weak, wire) = parse_entity_tag("W/\"xyzzy\"").unwrap();
1548 assert!(weak);
1549 assert_eq!(wire, "W/\"xyzzy\"");
1550 }
1551
1552 #[test]
1553 fn entity_tag_parses_empty_opaque() {
1554 let (weak, wire) = parse_entity_tag("\"\"").unwrap();
1556 assert!(!weak);
1557 assert_eq!(wire, "\"\"");
1558 }
1559
1560 #[test]
1561 fn entity_tag_rejects_unquoted_value() {
1562 assert!(parse_entity_tag("xyzzy").is_none());
1564 assert!(parse_entity_tag("W/xyzzy").is_none());
1565 }
1566
1567 #[test]
1568 fn entity_tag_weak_marker_is_case_sensitive() {
1569 assert!(parse_entity_tag("w/\"xyzzy\"").is_none());
1573 }
1574
1575 #[test]
1576 fn entity_tag_rejects_disallowed_inner_bytes() {
1577 assert!(parse_entity_tag("\"hello\"world\"").is_none());
1579 assert!(parse_entity_tag("\"tab\there\"").is_none()); }
1581
1582 #[test]
1583 fn imf_fixdate_parses_canonical_example() {
1584 let d = parse_imf_fixdate("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
1586 assert_eq!(d, (1994, 11, 6, 8, 49, 37));
1587 }
1588
1589 #[test]
1590 fn imf_fixdate_rejects_non_imf_forms() {
1591 assert!(parse_imf_fixdate("Sunday, 06-Nov-94 08:49:37 GMT").is_none());
1594 assert!(parse_imf_fixdate("Sun Nov 6 08:49:37 1994").is_none());
1595 assert!(parse_imf_fixdate("Sun, 06 Nov 1994 08:49:37").is_none());
1597 assert!(parse_imf_fixdate("Sun, 06 Foo 1994 08:49:37 GMT").is_none());
1599 }
1600
1601 #[test]
1602 fn imf_seconds_orders_within_one_second_correctly() {
1603 let lm = parse_imf_fixdate("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
1606 let dt_eq = parse_imf_fixdate("Sun, 06 Nov 1994 08:49:37 GMT").unwrap();
1607 let dt_p1 = parse_imf_fixdate("Sun, 06 Nov 1994 08:49:38 GMT").unwrap();
1608 let dt_m1 = parse_imf_fixdate("Sun, 06 Nov 1994 08:49:36 GMT").unwrap();
1609 assert!(imf_seconds(dt_eq) == imf_seconds(lm));
1610 assert!(imf_seconds(dt_p1) == imf_seconds(lm) + 1);
1611 assert!(imf_seconds(dt_m1) == imf_seconds(lm) - 1);
1612 }
1613
1614 #[test]
1615 fn derive_strong_validator_prefers_strong_etag() {
1616 let v = derive_strong_validator(
1617 Some("\"xyz\""),
1618 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1619 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1620 );
1621 assert_eq!(v, Some(StrongValidator::Etag("\"xyz\"".into())));
1622 }
1623
1624 #[test]
1625 fn derive_strong_validator_skips_weak_etag_falls_to_strong_last_modified() {
1626 let v = derive_strong_validator(
1629 Some("W/\"xyz\""),
1630 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1631 Some("Sun, 06 Nov 1994 08:49:42 GMT"),
1632 );
1633 assert_eq!(
1634 v,
1635 Some(StrongValidator::LastModified(
1636 "Sun, 06 Nov 1994 08:49:37 GMT".into()
1637 ))
1638 );
1639 }
1640
1641 #[test]
1642 fn derive_strong_validator_rejects_lm_when_date_within_one_second() {
1643 let v = derive_strong_validator(
1647 None,
1648 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1649 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1650 );
1651 assert_eq!(v, None);
1652 }
1653
1654 #[test]
1655 fn derive_strong_validator_rejects_lm_without_date() {
1656 let v = derive_strong_validator(None, Some("Sun, 06 Nov 1994 08:49:37 GMT"), None);
1659 assert_eq!(v, None);
1660 }
1661
1662 #[test]
1663 fn derive_strong_validator_handles_missing_headers() {
1664 assert_eq!(derive_strong_validator(None, None, None), None);
1665 }
1666
1667 #[test]
1668 fn derive_strong_validator_skips_malformed_etag_falls_through() {
1669 let v = derive_strong_validator(
1671 Some("not-quoted"),
1672 Some("Sun, 06 Nov 1994 08:49:37 GMT"),
1673 Some("Sun, 06 Nov 1994 08:49:42 GMT"),
1674 );
1675 assert_eq!(
1676 v,
1677 Some(StrongValidator::LastModified(
1678 "Sun, 06 Nov 1994 08:49:37 GMT".into()
1679 ))
1680 );
1681 }
1682
1683 fn spawn_server_capturing_get(
1687 head_resp: &'static [u8],
1688 get_resp: &'static [u8],
1689 ) -> (String, mpsc::Receiver<Vec<u8>>) {
1690 let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
1691 let port = listener.local_addr().unwrap().port();
1692 let (tx, rx) = mpsc::channel::<Vec<u8>>();
1693 thread::spawn(move || {
1694 for _ in 0..4 {
1695 let Ok((mut stream, _)) = listener.accept() else {
1696 break;
1697 };
1698 let mut buf = [0u8; 4096];
1699 use std::io::Read as _;
1700 let n = stream.read(&mut buf).unwrap_or(0);
1701 if n == 0 {
1702 continue;
1703 }
1704 let req = &buf[..n];
1705 let resp = if req.starts_with(b"HEAD ") {
1706 head_resp
1707 } else {
1708 let _ = tx.send(req.to_vec());
1711 get_resp
1712 };
1713 let _ = stream.write_all(resp);
1714 let _ = stream.flush();
1715 }
1716 });
1717 (format!("http://127.0.0.1:{port}/x"), rx)
1718 }
1719
1720 const HEAD_10B_WITH_STRONG_ETAG: &[u8] = b"HTTP/1.1 200 OK\r\n\
1723 Content-Length: 10\r\n\
1724 Accept-Ranges: bytes\r\n\
1725 ETag: \"v1\"\r\n\
1726 Connection: close\r\n\
1727 \r\n";
1728
1729 #[test]
1730 fn local_server_strong_etag_emits_if_range_header_on_get() {
1731 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1734 Content-Length: 10\r\n\
1735 Content-Range: bytes 0-9/10\r\n\
1736 Connection: close\r\n\
1737 \r\n\
1738 0123456789";
1739 let (uri, captured) = spawn_server_capturing_get(HEAD_10B_WITH_STRONG_ETAG, GET);
1740 let mut src = HttpSource::open(&uri).expect("open");
1741 let mut buf = [0u8; 10];
1742 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1743 let req = captured.recv().expect("captured GET request");
1744 let s = String::from_utf8_lossy(&req);
1745 assert!(
1746 s.to_ascii_lowercase().contains("if-range: \"v1\""),
1747 "GET did not carry If-Range; got:\n{s}"
1748 );
1749 }
1750
1751 const HEAD_10B_WITH_WEAK_ETAG: &[u8] = b"HTTP/1.1 200 OK\r\n\
1754 Content-Length: 10\r\n\
1755 Accept-Ranges: bytes\r\n\
1756 ETag: W/\"v1\"\r\n\
1757 Connection: close\r\n\
1758 \r\n";
1759
1760 #[test]
1761 fn local_server_weak_etag_does_not_emit_if_range_header() {
1762 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1765 Content-Length: 10\r\n\
1766 Content-Range: bytes 0-9/10\r\n\
1767 Connection: close\r\n\
1768 \r\n\
1769 0123456789";
1770 let (uri, captured) = spawn_server_capturing_get(HEAD_10B_WITH_WEAK_ETAG, GET);
1771 let mut src = HttpSource::open(&uri).expect("open");
1772 let mut buf = [0u8; 10];
1773 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1774 let req = captured.recv().expect("captured GET request");
1775 let s = String::from_utf8_lossy(&req);
1776 assert!(
1777 !s.to_ascii_lowercase().contains("if-range:"),
1778 "GET unexpectedly carried If-Range with a weak ETag; got:\n{s}"
1779 );
1780 }
1781
1782 #[test]
1783 fn local_server_200_with_if_range_set_is_fatal_mutation() {
1784 static GET: &[u8] = b"HTTP/1.1 200 OK\r\n\
1790 Content-Length: 10\r\n\
1791 Connection: close\r\n\
1792 \r\n\
1793 ABCDEFGHIJ";
1794 let (uri, _captured) = spawn_server_capturing_get(HEAD_10B_WITH_STRONG_ETAG, GET);
1795 let mut src = HttpSource::open(&uri).expect("open");
1796 let mut buf = [0u8; 10];
1797 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1798 let msg = err.to_string();
1799 assert!(
1800 msg.contains("If-Range"),
1801 "wrong error (no If-Range mention): {msg}"
1802 );
1803 assert!(
1804 msg.contains("changed since HEAD"),
1805 "wrong error (no mutation phrasing): {msg}"
1806 );
1807 }
1808
1809 #[test]
1810 fn local_server_no_validator_still_drains_prefix_on_200() {
1811 static GET: &[u8] = b"HTTP/1.1 200 OK\r\n\
1816 Content-Length: 10\r\n\
1817 Connection: close\r\n\
1818 \r\n\
1819 0123456789";
1820 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1821 let mut src = HttpSource::open(&uri).expect("open");
1822 std::io::Seek::seek(&mut src, SeekFrom::Start(4)).unwrap();
1823 let mut buf = [0u8; 6];
1824 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1825 assert_eq!(&buf, b"456789");
1826 }
1827
1828 #[test]
1831 fn local_server_200_fallback_content_length_mismatch_is_fatal() {
1832 static GET: &[u8] = b"HTTP/1.1 200 OK\r\n\
1843 Content-Length: 5\r\n\
1844 Connection: close\r\n\
1845 \r\n\
1846 01234";
1847 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1848 let mut src = HttpSource::open(&uri).expect("open");
1849 let mut buf = [0u8; 10];
1850 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1851 let msg = err.to_string();
1852 assert!(
1853 msg.contains("Content-Length 5"),
1854 "wrong error (no 'Content-Length 5'): {msg}"
1855 );
1856 assert!(
1857 msg.contains("HEAD-observed total 10"),
1858 "wrong error (no HEAD-observed mention): {msg}"
1859 );
1860 assert!(msg.contains("§8.6"), "wrong error (no §8.6 cite): {msg}");
1861 }
1862
1863 #[test]
1864 fn local_server_200_fallback_without_content_length_is_accepted() {
1865 static GET: &[u8] = b"HTTP/1.1 200 OK\r\n\
1871 Connection: close\r\n\
1872 \r\n\
1873 0123456789";
1874 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1875 let mut src = HttpSource::open(&uri).expect("open");
1876 std::io::Seek::seek(&mut src, SeekFrom::Start(4)).unwrap();
1877 let mut buf = [0u8; 6];
1878 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1879 assert_eq!(&buf, b"456789");
1880 }
1881
1882 #[test]
1883 fn local_server_206_content_length_mismatch_is_fatal() {
1884 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1895 Content-Length: 5\r\n\
1896 Content-Range: bytes 0-9/10\r\n\
1897 Connection: close\r\n\
1898 \r\n\
1899 01234";
1900 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1901 let mut src = HttpSource::open(&uri).expect("open");
1902 let mut buf = [0u8; 10];
1903 let err = std::io::Read::read_exact(&mut src, &mut buf).unwrap_err();
1904 let msg = err.to_string();
1905 assert!(
1906 msg.contains("Content-Length 5"),
1907 "wrong error (no 'Content-Length 5'): {msg}"
1908 );
1909 assert!(
1910 msg.contains("Content-Range span 10"),
1911 "wrong error (no 'Content-Range span 10'): {msg}"
1912 );
1913 assert!(msg.contains("§8.6"), "wrong error (no §8.6 cite): {msg}");
1914 }
1915
1916 #[test]
1917 fn local_server_206_matching_content_length_is_accepted() {
1918 static GET: &[u8] = b"HTTP/1.1 206 Partial Content\r\n\
1922 Content-Length: 10\r\n\
1923 Content-Range: bytes 0-9/10\r\n\
1924 Connection: close\r\n\
1925 \r\n\
1926 0123456789";
1927 let (uri, _done) = spawn_server(HEAD_10B_BYTES, GET);
1928 let mut src = HttpSource::open(&uri).expect("open");
1929 let mut buf = [0u8; 10];
1930 std::io::Read::read_exact(&mut src, &mut buf).expect("read");
1931 assert_eq!(&buf, b"0123456789");
1932 }
1933}