qualia_core_db/q42/volume/
range.rs1use 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#[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
43pub trait Q42RangeSource {
46 fn length(&self) -> io::Result<u64>;
47 fn read_range_into(&self, range: Q42ByteRange, out: &mut [u8]) -> io::Result<()>;
48}
49
50pub 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
89pub 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
108pub 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#[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 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#[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#[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}