copc_reader/
range_read.rs1use std::fs::File;
4use std::io::{Read, Seek, SeekFrom};
5
6use copc_core::{Error, Result};
7
8#[allow(clippy::len_without_is_empty)]
13pub trait RangeRead {
14 fn len(&mut self) -> Result<u64>;
16
17 fn read_range(&mut self, offset: u64, buf: &mut [u8]) -> Result<()>;
19}
20
21impl RangeRead for File {
22 fn len(&mut self) -> Result<u64> {
23 Ok(self
24 .metadata()
25 .map_err(|e| Error::io("stat COPC file", e))?
26 .len())
27 }
28
29 fn read_range(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
30 self.seek(SeekFrom::Start(offset))
31 .map_err(|e| Error::io("seek COPC range", e))?;
32 self.read_exact(buf)
33 .map_err(|e| Error::io("read COPC range", e))
34 }
35}
36
37#[cfg(feature = "http")]
42pub struct HttpRangeReader {
43 agent: ureq::Agent,
44 url: String,
45 len: Option<u64>,
46}
47
48#[cfg(feature = "http")]
49impl HttpRangeReader {
50 pub fn new(url: impl Into<String>) -> Self {
51 Self {
52 agent: ureq::Agent::new_with_defaults(),
53 url: url.into(),
54 len: None,
55 }
56 }
57}
58
59#[cfg(feature = "http")]
60impl RangeRead for HttpRangeReader {
61 fn len(&mut self) -> Result<u64> {
64 if let Some(len) = self.len {
65 return Ok(len);
66 }
67 let response = self
68 .agent
69 .get(&self.url)
70 .header("Range", "bytes=0-0")
71 .header("Accept-Encoding", "identity")
72 .call()
73 .map_err(|e| Error::InvalidInput(format!("GET {}: {e}", self.url)))?;
74 if response.status() != 206 {
75 return Err(Error::Unsupported(format!(
76 "GET {} returned status {}; the server must support HTTP range requests",
77 self.url,
78 response.status()
79 )));
80 }
81 let (start, end, len) = response
82 .headers()
83 .get("content-range")
84 .and_then(|value| value.to_str().ok())
85 .and_then(parse_content_range)
86 .ok_or_else(|| {
87 Error::InvalidData(format!(
88 "GET {} returned no usable Content-Range header",
89 self.url
90 ))
91 })?;
92 if (start, end) != (0, 0) {
93 return Err(Error::InvalidData(format!(
94 "GET {} returned Content-Range bytes {start}-{end}, expected bytes 0-0",
95 self.url
96 )));
97 }
98 if len == 0 {
99 return Err(Error::InvalidData(format!(
100 "GET {} returned a zero Content-Range total",
101 self.url
102 )));
103 }
104 let mut reader = response.into_body().into_reader();
105 let mut probe = [0u8; 1];
106 reader
107 .read_exact(&mut probe)
108 .map_err(|e| Error::io("read HTTP length probe body", e))?;
109 let mut trailing = [0u8; 1];
110 if reader
111 .read(&mut trailing)
112 .map_err(|e| Error::io("check HTTP length probe body", e))?
113 != 0
114 {
115 return Err(Error::InvalidData(format!(
116 "GET {} returned more bytes than its Content-Range",
117 self.url
118 )));
119 }
120 self.len = Some(len);
121 Ok(len)
122 }
123
124 fn read_range(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
125 if buf.is_empty() {
126 return Ok(());
127 }
128 let byte_len = u64::try_from(buf.len())
129 .map_err(|_| Error::InvalidInput("HTTP range length exceeds u64".into()))?;
130 let end_inclusive = offset
131 .checked_add(byte_len - 1)
132 .ok_or_else(|| Error::InvalidInput("HTTP range end overflows u64".into()))?;
133 let response = self
134 .agent
135 .get(&self.url)
136 .header("Range", format!("bytes={offset}-{end_inclusive}"))
137 .header("Accept-Encoding", "identity")
138 .call()
139 .map_err(|e| Error::InvalidInput(format!("GET {}: {e}", self.url)))?;
140 if response.status() != 206 {
141 return Err(Error::Unsupported(format!(
142 "GET {} returned status {}; the server must support HTTP range requests",
143 self.url,
144 response.status()
145 )));
146 }
147 let (response_start, response_end, response_len) = response
148 .headers()
149 .get("content-range")
150 .and_then(|value| value.to_str().ok())
151 .and_then(parse_content_range)
152 .ok_or_else(|| {
153 Error::InvalidData(format!(
154 "GET {} returned no usable Content-Range header",
155 self.url
156 ))
157 })?;
158 if (response_start, response_end) != (offset, end_inclusive) {
159 return Err(Error::InvalidData(format!(
160 "GET {} returned Content-Range bytes {response_start}-{response_end}, expected bytes {offset}-{end_inclusive}",
161 self.url
162 )));
163 }
164 if let Some(expected_len) = self.len {
165 if response_len != expected_len {
166 return Err(Error::InvalidData(format!(
167 "GET {} Content-Range total changed from {expected_len} to {response_len}",
168 self.url
169 )));
170 }
171 } else {
172 self.len = Some(response_len);
173 }
174 let mut reader = response.into_body().into_reader();
175 reader
176 .read_exact(buf)
177 .map_err(|e| Error::io("read HTTP range body", e))?;
178 let mut trailing = [0u8; 1];
179 if reader
180 .read(&mut trailing)
181 .map_err(|e| Error::io("check HTTP range body length", e))?
182 != 0
183 {
184 return Err(Error::InvalidData(format!(
185 "GET {} returned more bytes than its Content-Range",
186 self.url
187 )));
188 }
189 Ok(())
190 }
191}
192
193#[cfg(feature = "http")]
194fn parse_content_range(value: &str) -> Option<(u64, u64, u64)> {
195 let value = value.strip_prefix("bytes ")?;
196 let (range, total) = value.split_once('/')?;
197 let (start, end) = range.split_once('-')?;
198 let start = start.parse().ok()?;
199 let end = end.parse().ok()?;
200 let total = total.parse().ok()?;
201 (start <= end && end < total).then_some((start, end, total))
202}
203
204#[cfg(all(test, feature = "http"))]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn parses_strict_content_ranges() {
210 assert_eq!(Some((0, 0, 42)), parse_content_range("bytes 0-0/42"));
211 assert_eq!(None, parse_content_range("bytes 1-0/42"));
212 assert_eq!(None, parse_content_range("bytes 0-42/42"));
213 assert_eq!(None, parse_content_range("0-0/42"));
214 assert_eq!(None, parse_content_range("bytes 0-0/*"));
215 }
216}