Skip to main content

copc_reader/
range_read.rs

1//! Byte-range sources for remote and partial COPC reads.
2
3use std::fs::File;
4use std::io::{Read, Seek, SeekFrom};
5
6use copc_core::{Error, Result};
7
8/// A random-access byte source addressed by absolute offsets.
9///
10/// Implementations back [`crate::CopcRangeReader`], which fetches only the
11/// header, the hierarchy pages a query needs, and the selected point chunks.
12#[allow(clippy::len_without_is_empty)]
13pub trait RangeRead {
14    /// Total length of the source in bytes.
15    fn len(&mut self) -> Result<u64>;
16
17    /// Fill `buf` with the bytes at `offset..offset + buf.len()`.
18    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/// HTTP(S) byte-range source using `Range` requests.
38///
39/// Plain HTTP works out of the box; enable the crate's `http-tls` feature
40/// (which forwards to `ureq/rustls`) for HTTPS URLs.
41#[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    /// Probes the total length with a one-byte range request, which also
62    /// verifies up front that the server honors `Range` headers.
63    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}