1use std::io::{self, Read, Seek, SeekFrom};
21use std::sync::OnceLock;
22
23use oxideav_core::BytesSource;
24use oxideav_core::RuntimeContext;
25use oxideav_core::{Error, Result};
26use oxideav_source::SourceRegistry;
27use ureq::Agent;
28
29pub fn register(ctx: &mut RuntimeContext) {
34 register_source(&mut ctx.sources);
35}
36
37oxideav_core::register!("http", register);
38
39pub fn register_source(registry: &mut SourceRegistry) {
46 registry.register_bytes("http", open_http);
47 registry.register_bytes("https", open_http);
48}
49
50pub fn open_http(uri: &str) -> Result<Box<dyn BytesSource>> {
52 let src = HttpSource::open(uri)?;
53 Ok(Box::new(src))
54}
55
56fn agent() -> &'static Agent {
57 static A: OnceLock<Agent> = OnceLock::new();
58 A.get_or_init(|| Agent::config_builder().build().new_agent())
59}
60
61pub struct HttpSource {
63 uri: String,
64 total_len: u64,
65 pos: u64,
66 body: Option<Box<dyn Read + Send>>,
68}
69
70impl HttpSource {
71 pub fn open(uri: &str) -> Result<Self> {
72 let head = agent()
73 .head(uri)
74 .call()
75 .map_err(|e| Error::other(format!("HTTP HEAD {uri}: {e}")))?;
76
77 let status = head.status();
78 if !status.is_success() {
79 return Err(Error::other(format!("HTTP HEAD {uri}: status {status}")));
80 }
81 let headers = head.headers();
82 let total_len = headers
83 .get("content-length")
84 .and_then(|v| v.to_str().ok())
85 .and_then(|s| s.parse::<u64>().ok())
86 .ok_or_else(|| {
87 Error::Unsupported(format!("HTTP HEAD {uri}: missing Content-Length"))
88 })?;
89 let accept_ranges = headers
90 .get("accept-ranges")
91 .and_then(|v| v.to_str().ok())
92 .unwrap_or("");
93 if !accept_ranges.eq_ignore_ascii_case("bytes") {
94 return Err(Error::Unsupported(format!(
95 "HTTP HEAD {uri}: server does not advertise byte ranges (Accept-Ranges: '{accept_ranges}')"
96 )));
97 }
98 Ok(Self {
99 uri: uri.to_owned(),
100 total_len,
101 pos: 0,
102 body: None,
103 })
104 }
105
106 pub fn len(&self) -> u64 {
107 self.total_len
108 }
109
110 pub fn is_empty(&self) -> bool {
111 self.total_len == 0
112 }
113
114 fn issue_range(&mut self) -> io::Result<()> {
115 if self.pos >= self.total_len {
116 self.body = None;
117 return Ok(());
118 }
119 let range = format!("bytes={}-", self.pos);
120 let resp = agent()
121 .get(&self.uri)
122 .header("Range", &range)
123 .call()
124 .map_err(|e| io::Error::other(format!("HTTP GET {} {}: {e}", self.uri, range)))?;
125 let status = resp.status();
126 if !(status == 206 || status == 200) {
127 return Err(io::Error::other(format!(
128 "HTTP GET {} {}: status {status}",
129 self.uri, range
130 )));
131 }
132 let reader = resp.into_body().into_reader();
135 self.body = Some(Box::new(reader));
136 Ok(())
137 }
138}
139
140impl Read for HttpSource {
141 fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
142 if out.is_empty() {
143 return Ok(0);
144 }
145 if self.pos >= self.total_len {
146 return Ok(0);
147 }
148 loop {
149 if self.body.is_none() {
150 self.issue_range()?;
151 }
152 let body = self.body.as_mut().expect("body just issued");
153 let n = body.read(out)?;
154 if n > 0 {
155 self.pos += n as u64;
156 return Ok(n);
157 }
158 self.body = None;
159 if self.pos >= self.total_len {
160 return Ok(0);
161 }
162 }
163 }
164}
165
166impl Seek for HttpSource {
167 fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
168 let new_pos: u64 = match from {
169 SeekFrom::Start(n) => n,
170 SeekFrom::Current(d) => add_signed(self.pos, d)?,
171 SeekFrom::End(d) => add_signed(self.total_len, d)?,
172 };
173 if new_pos > self.total_len {
174 return Err(io::Error::new(io::ErrorKind::InvalidInput, "seek past end"));
175 }
176 if new_pos != self.pos {
177 self.body = None;
178 self.pos = new_pos;
179 }
180 Ok(new_pos)
181 }
182}
183
184fn add_signed(base: u64, delta: i64) -> io::Result<u64> {
185 if delta >= 0 {
186 base.checked_add(delta as u64)
187 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow"))
188 } else {
189 base.checked_sub(delta.unsigned_abs())
190 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek before start"))
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::collections::BTreeSet;
198
199 #[test]
200 fn register_via_runtime_context_installs_http_and_https_schemes() {
201 let mut ctx = RuntimeContext::new();
202 register(&mut ctx);
203 let schemes: BTreeSet<&str> = ctx.sources.schemes().collect();
204 assert!(
205 schemes.contains("http"),
206 "register did not install http; got {schemes:?}"
207 );
208 assert!(
209 schemes.contains("https"),
210 "register did not install https; got {schemes:?}"
211 );
212 }
213
214 #[test]
215 fn register_source_installs_http_and_https_schemes_on_bare_registry() {
216 let mut reg = SourceRegistry::new();
217 register_source(&mut reg);
218 let schemes: BTreeSet<&str> = reg.schemes().collect();
219 assert!(schemes.contains("http"));
220 assert!(schemes.contains("https"));
221 }
222}