Skip to main content

qualia_core_db/q42/volume/
range.rs

1//! Transport-neutral, caller-buffered byte-range access.
2//!
3//! HTTP gateways and IPFS partial-CAR retrieval must obey this same exact-range
4//! contract. The local file adapter is both useful itself and the reference
5//! implementation for response-length and boundary validation.
6
7use std::fs::File;
8use std::io::{self, Read, Seek, SeekFrom};
9use std::path::Path;
10use std::sync::Mutex;
11
12use sha2::{Digest, Sha256};
13
14#[cfg(not(target_arch = "wasm32"))]
15use super::manifest::{Q42SegmentRangeFactory, Q42VolumeSegment};
16
17fn invalid(message: impl Into<String>) -> io::Error {
18    io::Error::new(io::ErrorKind::InvalidInput, message.into())
19}
20
21/// A half-open byte interval, represented without an end-offset overflow.
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub struct Q42ByteRange {
24    pub offset: u64,
25    pub length: usize,
26}
27
28impl Q42ByteRange {
29    pub fn end(self) -> io::Result<u64> {
30        self.offset
31            .checked_add(self.length as u64)
32            .ok_or_else(|| invalid("Q42 byte range overflows u64"))
33    }
34
35    pub fn validate_for(self, source_length: u64) -> io::Result<()> {
36        if self.end()? > source_length {
37            return Err(invalid("Q42 byte range exceeds source length"));
38        }
39        Ok(())
40    }
41}
42
43/// Exact, caller-buffered random access. Implementations must fail if the
44/// source returns a short response; partial data is never treated as success.
45pub trait Q42RangeSource {
46    fn length(&self) -> io::Result<u64>;
47    fn read_range_into(&self, range: Q42ByteRange, out: &mut [u8]) -> io::Result<()>;
48}
49
50/// Native local-file implementation. The mutex serialises seek/read pairs;
51/// high-concurrency remote adapters should instead use independent requests.
52pub struct LocalFileRangeSource {
53    length: u64,
54    file: Mutex<File>,
55}
56
57impl LocalFileRangeSource {
58    pub fn open(path: &Path) -> io::Result<Self> {
59        let file = File::open(path)?;
60        let length = file.metadata()?.len();
61        Ok(Self {
62            length,
63            file: Mutex::new(file),
64        })
65    }
66}
67
68impl Q42RangeSource for LocalFileRangeSource {
69    fn length(&self) -> io::Result<u64> {
70        Ok(self.length)
71    }
72
73    fn read_range_into(&self, range: Q42ByteRange, out: &mut [u8]) -> io::Result<()> {
74        if out.len() != range.length {
75            return Err(invalid(
76                "Q42 range output buffer length does not match request",
77            ));
78        }
79        range.validate_for(self.length)?;
80        let mut file = self
81            .file
82            .lock()
83            .map_err(|_| io::Error::other("Q42 range source lock poisoned"))?;
84        file.seek(SeekFrom::Start(range.offset))?;
85        file.read_exact(out)
86    }
87}
88
89/// Validate an HTTP/IPFS adapter response before it is accepted by a reader.
90/// `content_range_start` is the start declared by `Content-Range` or the
91/// verified CAR block offset; `returned_len` is its actual payload size.
92pub fn validate_exact_range_response(
93    requested: Q42ByteRange,
94    source_length: u64,
95    content_range_start: u64,
96    returned_len: usize,
97) -> io::Result<()> {
98    requested.validate_for(source_length)?;
99    if content_range_start != requested.offset || returned_len != requested.length {
100        return Err(io::Error::new(
101            io::ErrorKind::InvalidData,
102            "range response does not exactly match the requested Q42 bytes",
103        ));
104    }
105    Ok(())
106}
107
108/// Stream the complete immutable source through a caller-provided scratch
109/// buffer and compare it to the SHA-256 committed by the root descriptor.
110/// This is the bounded verification path for a trusted HTTP/IPFS gateway;
111/// partial-CAR block proofs can later authenticate individual ranges sooner.
112pub fn verify_source_sha256<S: Q42RangeSource>(
113    source: &S,
114    expected: &[u8; 32],
115    scratch: &mut [u8],
116) -> io::Result<()> {
117    if scratch.is_empty() {
118        return Err(invalid(
119            "Q42 verification requires a non-empty scratch buffer",
120        ));
121    }
122    let length = source.length()?;
123    let mut hasher = Sha256::new();
124    let mut offset = 0u64;
125    while offset < length {
126        let count = usize::try_from((length - offset).min(scratch.len() as u64))
127            .map_err(|_| invalid("Q42 verification chunk does not fit platform"))?;
128        source.read_range_into(
129            Q42ByteRange {
130                offset,
131                length: count,
132            },
133            &mut scratch[..count],
134        )?;
135        hasher.update(&scratch[..count]);
136        offset += count as u64;
137    }
138    let actual: [u8; 32] = hasher.finalize().into();
139    if &actual != expected {
140        return Err(io::Error::new(
141            io::ErrorKind::InvalidData,
142            "Q42 source SHA-256 differs from root manifest",
143        ));
144    }
145    Ok(())
146}
147
148/// Native HTTP byte-range source for trusted gateways. It never accepts a
149/// `200 OK` full-body fallback: large Q42 access must stay range-bounded.
150#[cfg(not(target_arch = "wasm32"))]
151pub struct HttpRangeSource {
152    client: reqwest::blocking::Client,
153    url: reqwest::Url,
154    length: u64,
155}
156
157#[cfg(not(target_arch = "wasm32"))]
158impl HttpRangeSource {
159    pub fn new(url: &str, length: u64) -> io::Result<Self> {
160        if length == 0 {
161            return Err(invalid("Q42 HTTP source length must be non-zero"));
162        }
163        let url = reqwest::Url::parse(url)
164            .map_err(|error| invalid(format!("invalid Q42 HTTP URL: {error}")))?;
165        if url.scheme() != "https" && url.scheme() != "http" {
166            return Err(invalid("Q42 HTTP source must use http or https"));
167        }
168        let client = reqwest::blocking::Client::builder()
169            .connect_timeout(std::time::Duration::from_secs(10))
170            .timeout(std::time::Duration::from_secs(30))
171            .build()
172            .map_err(|error| io::Error::other(format!("build Q42 HTTP client: {error}")))?;
173        Ok(Self {
174            client,
175            url,
176            length,
177        })
178    }
179
180    /// Discover an immutable source length without downloading its body. This
181    /// is a cold construction operation used for a root URL (including IPNS).
182    /// Gateways that omit `Content-Length` on HEAD must still prove a one-byte
183    /// `206` range response before they are accepted.
184    pub fn discover(url: &str) -> io::Result<Self> {
185        let url = reqwest::Url::parse(url)
186            .map_err(|error| invalid(format!("invalid Q42 HTTP URL: {error}")))?;
187        if url.scheme() != "https" && url.scheme() != "http" {
188            return Err(invalid("Q42 HTTP source must use http or https"));
189        }
190        let client = reqwest::blocking::Client::builder()
191            .connect_timeout(std::time::Duration::from_secs(10))
192            .timeout(std::time::Duration::from_secs(30))
193            .build()
194            .map_err(|error| io::Error::other(format!("build Q42 HTTP client: {error}")))?;
195        if let Ok(response) = client.head(url.clone()).send() {
196            if response.status().is_success() {
197                if let Some(length) = response.content_length().filter(|length| *length != 0) {
198                    return Ok(Self {
199                        client,
200                        url,
201                        length,
202                    });
203                }
204            }
205        }
206        let mut response = client
207            .get(url.clone())
208            .header(reqwest::header::RANGE, "bytes=0-0")
209            .send()
210            .map_err(|error| io::Error::other(format!("discover Q42 range length: {error}")))?;
211        if response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
212            return Err(invalid("Q42 gateway cannot prove byte-range source length"));
213        }
214        let header = response
215            .headers()
216            .get(reqwest::header::CONTENT_RANGE)
217            .and_then(|value| value.to_str().ok())
218            .ok_or_else(|| invalid("Q42 discovery response has no valid Content-Range"))?;
219        let (start, end, length) = parse_content_range(header)?;
220        if start != 0 || end != 0 {
221            return Err(invalid("Q42 discovery range was not exactly bytes 0-0"));
222        }
223        let mut first = [0u8; 1];
224        response.read_exact(&mut first)?;
225        let mut extra = [0u8; 1];
226        if response.read(&mut extra)? != 0 {
227            return Err(invalid("Q42 discovery response contains extra bytes"));
228        }
229        Ok(Self {
230            client,
231            url,
232            length,
233        })
234    }
235}
236
237#[cfg(not(target_arch = "wasm32"))]
238impl Q42RangeSource for HttpRangeSource {
239    fn length(&self) -> io::Result<u64> {
240        Ok(self.length)
241    }
242
243    fn read_range_into(&self, range: Q42ByteRange, out: &mut [u8]) -> io::Result<()> {
244        if out.len() != range.length {
245            return Err(invalid(
246                "Q42 range output buffer length does not match request",
247            ));
248        }
249        range.validate_for(self.length)?;
250        let end = range
251            .end()?
252            .checked_sub(1)
253            .ok_or_else(|| invalid("Q42 HTTP range may not be empty"))?;
254        let response = self
255            .client
256            .get(self.url.clone())
257            .header(
258                reqwest::header::RANGE,
259                format!("bytes={}-{}", range.offset, end),
260            )
261            .send()
262            .map_err(|error| io::Error::other(format!("fetch Q42 range: {error}")))?;
263        if response.status() != reqwest::StatusCode::PARTIAL_CONTENT {
264            return Err(io::Error::new(
265                io::ErrorKind::InvalidData,
266                "Q42 gateway did not return HTTP 206 Partial Content",
267            ));
268        }
269        let header = response
270            .headers()
271            .get(reqwest::header::CONTENT_RANGE)
272            .and_then(|value| value.to_str().ok())
273            .ok_or_else(|| {
274                io::Error::new(
275                    io::ErrorKind::InvalidData,
276                    "Q42 range response has no valid Content-Range",
277                )
278            })?;
279        let (start, returned_end, total) = parse_content_range(header)?;
280        if total != self.length
281            || returned_end
282                .checked_add(1)
283                .and_then(|value| value.checked_sub(start))
284                .and_then(|value| usize::try_from(value).ok())
285                != Some(range.length)
286        {
287            return Err(io::Error::new(
288                io::ErrorKind::InvalidData,
289                "Q42 Content-Range does not match catalog length",
290            ));
291        }
292        let mut response = response;
293        response
294            .read_exact(out)
295            .map_err(|error| io::Error::other(format!("read Q42 range body: {error}")))?;
296        let mut extra = [0u8; 1];
297        if response
298            .read(&mut extra)
299            .map_err(|error| io::Error::other(format!("read Q42 range tail: {error}")))?
300            != 0
301        {
302            return Err(io::Error::new(
303                io::ErrorKind::InvalidData,
304                "Q42 range response contains extra bytes",
305            ));
306        }
307        validate_exact_range_response(range, self.length, start, out.len())
308    }
309}
310
311#[cfg(not(target_arch = "wasm32"))]
312pub fn ipfs_gateway_range_source(
313    gateway: &str,
314    cid: &str,
315    length: u64,
316) -> io::Result<HttpRangeSource> {
317    if cid.is_empty() || !cid.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
318        return Err(invalid("IPFS CID must be a non-empty base32/base58 token"));
319    }
320    let gateway = gateway.trim_end_matches('/');
321    HttpRangeSource::new(&format!("{gateway}/ipfs/{cid}"), length)
322}
323
324/// Construct a range source for an IPNS-published root. Resolution is bounded
325/// by the HTTP adapter's 10-second connect and 30-second request timeouts;
326/// after the root manifest is read, child segments must use immutable CIDs.
327#[cfg(not(target_arch = "wasm32"))]
328pub fn ipns_gateway_range_source(gateway: &str, name: &str) -> io::Result<HttpRangeSource> {
329    if name.is_empty()
330        || !name
331            .bytes()
332            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
333    {
334        return Err(invalid("IPNS name must be an ASCII name without a path"));
335    }
336    let gateway = gateway.trim_end_matches('/');
337    HttpRangeSource::discover(&format!("{gateway}/ipns/{name}"))
338}
339
340/// Opens manifest `ipfs://CID` child locators through one configured gateway.
341/// The CID and byte length remain manifest-committed; the gateway is merely a
342/// transport route and cannot substitute a different-length object.
343#[cfg(not(target_arch = "wasm32"))]
344pub struct IpfsGatewaySegmentFactory {
345    gateway: String,
346}
347
348#[cfg(not(target_arch = "wasm32"))]
349impl IpfsGatewaySegmentFactory {
350    pub fn new(gateway: &str) -> io::Result<Self> {
351        let gateway = gateway.trim_end_matches('/');
352        let url = reqwest::Url::parse(gateway)
353            .map_err(|error| invalid(format!("invalid IPFS gateway URL: {error}")))?;
354        if url.scheme() != "https" && url.scheme() != "http" {
355            return Err(invalid("IPFS gateway must use http or https"));
356        }
357        Ok(Self {
358            gateway: gateway.to_owned(),
359        })
360    }
361}
362
363#[cfg(not(target_arch = "wasm32"))]
364impl Q42SegmentRangeFactory for IpfsGatewaySegmentFactory {
365    type Source = HttpRangeSource;
366
367    fn open_segment(&self, segment: &Q42VolumeSegment) -> io::Result<Self::Source> {
368        let cid = segment
369            .ipfs_cid()
370            .ok_or_else(|| invalid("IPFS gateway factory requires an ipfs://CID locator"))?;
371        ipfs_gateway_range_source(&self.gateway, cid, segment.byte_length)
372    }
373}
374
375#[cfg(not(target_arch = "wasm32"))]
376impl super::manifest::Q42LexiconRangeFactory for IpfsGatewaySegmentFactory {
377    type Source = HttpRangeSource;
378
379    fn open_lexicon_segment(
380        &self,
381        segment: &super::manifest::Q42LexiconSegment,
382    ) -> io::Result<Self::Source> {
383        let cid = segment.locator.strip_prefix("ipfs://").ok_or_else(|| {
384            invalid("IPFS gateway factory requires an ipfs://CID lexicon locator")
385        })?;
386        ipfs_gateway_range_source(&self.gateway, cid, segment.byte_length)
387    }
388}
389
390#[cfg(not(target_arch = "wasm32"))]
391fn parse_content_range(value: &str) -> io::Result<(u64, u64, u64)> {
392    let body = value
393        .strip_prefix("bytes ")
394        .ok_or_else(|| invalid("unsupported Content-Range unit"))?;
395    let (range, total) = body
396        .split_once('/')
397        .ok_or_else(|| invalid("malformed Content-Range"))?;
398    let (start, end) = range
399        .split_once('-')
400        .ok_or_else(|| invalid("malformed Content-Range interval"))?;
401    let start = start
402        .parse()
403        .map_err(|_| invalid("invalid Content-Range start"))?;
404    let end = end
405        .parse()
406        .map_err(|_| invalid("invalid Content-Range end"))?;
407    let total = total
408        .parse()
409        .map_err(|_| invalid("invalid Content-Range total"))?;
410    if start > end || end >= total {
411        return Err(invalid("Content-Range lies outside source"));
412    }
413    Ok((start, end, total))
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::specialized_libs::computational_geometry::allocation_counter::assert_zero_alloc;
420    use std::io::Write;
421    use tempfile::NamedTempFile;
422
423    #[test]
424    fn local_source_requires_exact_bounded_ranges() {
425        let mut file = NamedTempFile::new().unwrap();
426        file.write_all(b"0123456789").unwrap();
427        let source = LocalFileRangeSource::open(file.path()).unwrap();
428        let mut out = [0u8; 4];
429        source
430            .read_range_into(
431                Q42ByteRange {
432                    offset: 3,
433                    length: 4,
434                },
435                &mut out,
436            )
437            .unwrap();
438        assert_eq!(&out, b"3456");
439        assert!(source
440            .read_range_into(
441                Q42ByteRange {
442                    offset: 9,
443                    length: 2
444                },
445                &mut [0; 2]
446            )
447            .is_err());
448        assert!(validate_exact_range_response(
449            Q42ByteRange {
450                offset: 3,
451                length: 4
452            },
453            10,
454            4,
455            4
456        )
457        .is_err());
458    }
459
460    #[test]
461    fn source_hash_verification_is_bounded_and_fail_closed() {
462        let mut file = NamedTempFile::new().unwrap();
463        file.write_all(b"0123456789").unwrap();
464        let source = LocalFileRangeSource::open(file.path()).unwrap();
465        let expected: [u8; 32] = Sha256::digest(b"0123456789").into();
466        let mut scratch = [0u8; 3];
467        verify_source_sha256(&source, &expected, &mut scratch).unwrap();
468        assert!(verify_source_sha256(&source, &[0; 32], &mut scratch).is_err());
469    }
470
471    #[test]
472    fn local_range_and_digest_hot_loops_are_zero_heap() {
473        let mut file = NamedTempFile::new().unwrap();
474        file.write_all(b"0123456789").unwrap();
475        let source = LocalFileRangeSource::open(file.path()).unwrap();
476        let expected: [u8; 32] = Sha256::digest(b"0123456789").into();
477        let mut read_buffer = [0u8; 4];
478        let mut digest_buffer = [0u8; 3];
479        assert_zero_alloc("q42_local_range_read", || {
480            source
481                .read_range_into(
482                    Q42ByteRange {
483                        offset: 3,
484                        length: 4,
485                    },
486                    &mut read_buffer,
487                )
488                .unwrap();
489        });
490        assert_zero_alloc("q42_source_sha256", || {
491            verify_source_sha256(&source, &expected, &mut digest_buffer).unwrap();
492        });
493    }
494
495    #[cfg(not(target_arch = "wasm32"))]
496    #[test]
497    fn content_range_parser_rejects_mismatched_or_invalid_bounds() {
498        assert_eq!(parse_content_range("bytes 5-8/10").unwrap(), (5, 8, 10));
499        assert!(parse_content_range("bytes 8-5/10").is_err());
500        assert!(parse_content_range("bytes 5-10/10").is_err());
501        assert!(parse_content_range("items 5-8/10").is_err());
502    }
503
504    #[cfg(not(target_arch = "wasm32"))]
505    #[test]
506    fn ipfs_factory_requires_cid_locators_without_contacting_the_gateway() {
507        let factory = IpfsGatewaySegmentFactory::new("https://gateway.example").unwrap();
508        let local = Q42VolumeSegment {
509            locator: "segment.q42".into(),
510            byte_length: 1,
511            first_object_hash: 1,
512            last_object_hash: 1,
513            quin_count: 1,
514            sha256: [0; 32],
515        };
516        assert!(factory.open_segment(&local).is_err());
517        assert!(ipns_gateway_range_source("https://gateway.example", "bad/name").is_err());
518    }
519}