1use std::fs::File;
4use std::io::{self, Read};
5use std::path::{Component, Path, PathBuf};
6
7use sha2::{Digest, Sha256};
8
9use super::super::{
10 Q42VerificationReceipt, Q42Volume, MAX_COMPRESSED_SUPERBLOCK_SIZE, SUPERBLOCK_SIZE,
11};
12use super::range::{verify_source_sha256, Q42RangeSource};
13use super::range_volume::{
14 Q42RangeQueryCursor, Q42RangeQueryPage, Q42RangeQueryPlan, Q42RangeVolume,
15};
16
17pub const VOLUME_MANIFEST_MAGIC: [u8; 8] = *b"Q42VOL\0\0";
18pub const VOLUME_MANIFEST_VERSION: u16 = 2;
19pub const MAX_VOLUME_MANIFEST_BYTES: usize = 4 * 1024 * 1024;
20pub const MAX_VOLUME_SEGMENTS: usize = 65_536;
21const HEADER_BYTES: usize = 32;
22const ENTRY_FIXED_BYTES: usize = 66;
23const LEX_ENTRY_FIXED_BYTES: usize = 58;
24
25fn invalid(message: impl Into<String>) -> io::Error {
26 io::Error::new(io::ErrorKind::InvalidData, message.into())
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct Q42VolumeSegment {
32 pub locator: String,
35 pub byte_length: u64,
36 pub first_object_hash: u64,
37 pub last_object_hash: u64,
38 pub quin_count: u64,
39 pub sha256: [u8; 32],
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct Q42LexiconSegment {
48 pub locator: String,
49 pub byte_length: u64,
50 pub first_hash: u64,
51 pub last_hash: u64,
52 pub sha256: [u8; 32],
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct Q42VolumeManifest {
58 pub generation: u64,
59 pub segments: Vec<Q42VolumeSegment>,
60 pub lexicon_segments: Vec<Q42LexiconSegment>,
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct Q42SegmentMatchRange {
66 pub start: usize,
67 pub end: usize,
68}
69
70impl Q42SegmentMatchRange {
71 pub fn len(self) -> usize {
72 self.end - self.start
73 }
74
75 pub fn is_empty(self) -> bool {
76 self.start == self.end
77 }
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
82pub struct Q42SegmentMatchPage {
83 pub range: Q42SegmentMatchRange,
84 pub returned: usize,
85 pub next_cursor: Option<usize>,
86}
87
88impl Q42VolumeManifest {
89 pub fn encode(&self) -> io::Result<Vec<u8>> {
90 self.validate()?;
91 let mut bytes = Vec::with_capacity(
92 HEADER_BYTES
93 + self.segments.len() * ENTRY_FIXED_BYTES
94 + self.lexicon_segments.len() * LEX_ENTRY_FIXED_BYTES,
95 );
96 bytes.extend_from_slice(&VOLUME_MANIFEST_MAGIC);
97 bytes.extend_from_slice(&VOLUME_MANIFEST_VERSION.to_le_bytes());
98 bytes.extend_from_slice(&0u16.to_le_bytes());
99 bytes.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
100 bytes.extend_from_slice(&self.generation.to_le_bytes());
101 bytes.extend_from_slice(&(self.lexicon_segments.len() as u64).to_le_bytes());
102 for segment in &self.segments {
103 let locator = segment.locator.as_bytes();
104 let locator_len = u16::try_from(locator.len()).map_err(|_| {
105 io::Error::new(
106 io::ErrorKind::InvalidInput,
107 "Q42 segment locator exceeds u16 length",
108 )
109 })?;
110 bytes.extend_from_slice(&segment.byte_length.to_le_bytes());
111 bytes.extend_from_slice(&segment.first_object_hash.to_le_bytes());
112 bytes.extend_from_slice(&segment.last_object_hash.to_le_bytes());
113 bytes.extend_from_slice(&segment.quin_count.to_le_bytes());
114 bytes.extend_from_slice(&segment.sha256);
115 bytes.extend_from_slice(&locator_len.to_le_bytes());
116 bytes.extend_from_slice(locator);
117 }
118 for segment in &self.lexicon_segments {
119 let locator = segment.locator.as_bytes();
120 let locator_len = u16::try_from(locator.len()).map_err(|_| {
121 io::Error::new(
122 io::ErrorKind::InvalidInput,
123 "Q42 lexicon segment locator exceeds u16 length",
124 )
125 })?;
126 bytes.extend_from_slice(&segment.byte_length.to_le_bytes());
127 bytes.extend_from_slice(&segment.first_hash.to_le_bytes());
128 bytes.extend_from_slice(&segment.last_hash.to_le_bytes());
129 bytes.extend_from_slice(&segment.sha256);
130 bytes.extend_from_slice(&locator_len.to_le_bytes());
131 bytes.extend_from_slice(locator);
132 }
133 if bytes.len() > MAX_VOLUME_MANIFEST_BYTES {
134 return Err(io::Error::new(
135 io::ErrorKind::InvalidInput,
136 "Q42 volume manifest exceeds the 4 MiB front-matter ceiling",
137 ));
138 }
139 Ok(bytes)
140 }
141
142 pub fn decode(bytes: &[u8]) -> io::Result<Self> {
143 if bytes.len() < HEADER_BYTES || bytes.len() > MAX_VOLUME_MANIFEST_BYTES {
144 return Err(invalid("invalid Q42 volume manifest length"));
145 }
146 if bytes[0..8] != VOLUME_MANIFEST_MAGIC {
147 return Err(invalid("invalid Q42 volume manifest magic"));
148 }
149 let version = u16::from_le_bytes(bytes[8..10].try_into().unwrap());
150 if version != 1 && version != VOLUME_MANIFEST_VERSION {
151 return Err(invalid("unsupported Q42 volume manifest version"));
152 }
153 let count = u32::from_le_bytes(bytes[12..16].try_into().unwrap()) as usize;
154 if count == 0 || count > MAX_VOLUME_SEGMENTS {
155 return Err(invalid("invalid Q42 volume manifest segment count"));
156 }
157 let generation = u64::from_le_bytes(bytes[16..24].try_into().unwrap());
158 let lexicon_count = if version == 1 {
159 0
160 } else {
161 usize::try_from(u64::from_le_bytes(bytes[24..32].try_into().unwrap()))
162 .map_err(|_| invalid("Q42 lexicon segment count exceeds platform"))?
163 };
164 let mut offset = HEADER_BYTES;
165 let mut segments = Vec::with_capacity(count);
166 for _ in 0..count {
167 let fixed_end = offset
168 .checked_add(ENTRY_FIXED_BYTES)
169 .ok_or_else(|| invalid("manifest entry overflow"))?;
170 if fixed_end > bytes.len() {
171 return Err(invalid("truncated Q42 volume manifest entry"));
172 }
173 let byte_length = u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap());
174 let first_object_hash =
175 u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap());
176 let last_object_hash =
177 u64::from_le_bytes(bytes[offset + 16..offset + 24].try_into().unwrap());
178 let quin_count =
179 u64::from_le_bytes(bytes[offset + 24..offset + 32].try_into().unwrap());
180 let sha256 = bytes[offset + 32..offset + 64].try_into().unwrap();
181 offset = fixed_end;
182 let locator_len =
183 u16::from_le_bytes(bytes[offset - 2..offset].try_into().unwrap()) as usize;
184 let locator_end = offset
185 .checked_add(locator_len)
186 .ok_or_else(|| invalid("manifest locator overflow"))?;
187 if locator_end > bytes.len() {
188 return Err(invalid("truncated Q42 segment locator"));
189 }
190 let locator = std::str::from_utf8(&bytes[offset..locator_end])
191 .map_err(|_| invalid("Q42 segment locator is not UTF-8"))?
192 .to_owned();
193 segments.push(Q42VolumeSegment {
194 locator,
195 byte_length,
196 first_object_hash,
197 last_object_hash,
198 quin_count,
199 sha256,
200 });
201 offset = locator_end;
202 }
203 let mut lexicon_segments = Vec::with_capacity(lexicon_count);
204 for _ in 0..lexicon_count {
205 let fixed_end = offset
206 .checked_add(LEX_ENTRY_FIXED_BYTES)
207 .ok_or_else(|| invalid("lexicon manifest entry overflow"))?;
208 if fixed_end > bytes.len() {
209 return Err(invalid("truncated Q42 lexicon manifest entry"));
210 }
211 let byte_length = u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap());
212 let first_hash = u64::from_le_bytes(bytes[offset + 8..offset + 16].try_into().unwrap());
213 let last_hash = u64::from_le_bytes(bytes[offset + 16..offset + 24].try_into().unwrap());
214 let sha256 = bytes[offset + 24..offset + 56].try_into().unwrap();
215 let locator_len =
216 u16::from_le_bytes(bytes[offset + 56..fixed_end].try_into().unwrap()) as usize;
217 offset = fixed_end;
218 let locator_end = offset
219 .checked_add(locator_len)
220 .ok_or_else(|| invalid("Q42 lexicon locator overflow"))?;
221 if locator_end > bytes.len() {
222 return Err(invalid("truncated Q42 lexicon segment locator"));
223 }
224 let locator = std::str::from_utf8(&bytes[offset..locator_end])
225 .map_err(|_| invalid("Q42 lexicon locator is not UTF-8"))?
226 .to_owned();
227 lexicon_segments.push(Q42LexiconSegment {
228 locator,
229 byte_length,
230 first_hash,
231 last_hash,
232 sha256,
233 });
234 offset = locator_end;
235 }
236 if offset != bytes.len() {
237 return Err(invalid("Q42 volume manifest has trailing bytes"));
238 }
239 let manifest = Self {
240 generation,
241 segments,
242 lexicon_segments,
243 };
244 manifest.validate()?;
245 Ok(manifest)
246 }
247
248 pub fn validate(&self) -> io::Result<()> {
249 if self.segments.is_empty() || self.segments.len() > MAX_VOLUME_SEGMENTS {
250 return Err(io::Error::new(
251 io::ErrorKind::InvalidInput,
252 "Q42 volume must contain 1..=65536 segments",
253 ));
254 }
255 let mut previous_last = None;
256 for segment in &self.segments {
257 validate_segment_locator(&segment.locator)?;
258 if segment.byte_length == 0
259 || segment.quin_count == 0
260 || segment.first_object_hash > segment.last_object_hash
261 {
262 return Err(io::Error::new(
263 io::ErrorKind::InvalidInput,
264 "Q42 segment metadata is invalid",
265 ));
266 }
267 if previous_last.is_some_and(|last| segment.first_object_hash < last) {
268 return Err(io::Error::new(
269 io::ErrorKind::InvalidInput,
270 "Q42 segments are not globally object-sorted",
271 ));
272 }
273 previous_last = Some(segment.last_object_hash);
274 }
275 let mut previous_last = None;
276 for segment in &self.lexicon_segments {
277 validate_segment_locator(&segment.locator)?;
278 if segment.byte_length == 0
279 || segment.first_hash > segment.last_hash
280 || previous_last.is_some_and(|last| segment.first_hash <= last)
281 {
282 return Err(io::Error::new(
283 io::ErrorKind::InvalidInput,
284 "Q42 lexicon segments are invalid or overlap",
285 ));
286 }
287 previous_last = Some(segment.last_hash);
288 }
289 Ok(())
290 }
291
292 pub fn segment_range_for_object(&self, object_hash: u64) -> Option<Q42SegmentMatchRange> {
295 let mut lo = 0usize;
296 let mut hi = self.segments.len();
297 while lo < hi {
298 let mid = lo + (hi - lo) / 2;
299 if self.segments[mid].last_object_hash < object_hash {
300 lo = mid + 1;
301 } else {
302 hi = mid;
303 }
304 }
305 let start = lo;
306 if start == self.segments.len() || self.segments[start].first_object_hash > object_hash {
307 return None;
308 }
309 lo = start;
310 hi = self.segments.len();
311 while lo < hi {
312 let mid = lo + (hi - lo) / 2;
313 if self.segments[mid].first_object_hash <= object_hash {
314 lo = mid + 1;
315 } else {
316 hi = mid;
317 }
318 }
319 Some(Q42SegmentMatchRange { start, end: lo })
320 }
321
322 pub fn segment_indices_for_object_into(
325 &self,
326 object_hash: u64,
327 cursor: usize,
328 out: &mut [usize],
329 ) -> io::Result<Option<Q42SegmentMatchPage>> {
330 let Some(range) = self.segment_range_for_object(object_hash) else {
331 return Ok(None);
332 };
333 if cursor > range.len() {
334 return Err(io::Error::new(
335 io::ErrorKind::InvalidInput,
336 "Q42 manifest segment cursor is beyond the matching interval",
337 ));
338 }
339 if out.is_empty() && cursor < range.len() {
340 return Err(io::Error::new(
341 io::ErrorKind::InvalidInput,
342 "Q42 manifest segment page requires at least one output slot",
343 ));
344 }
345 let returned = (range.len() - cursor).min(out.len());
346 for (offset, slot) in out.iter_mut().take(returned).enumerate() {
347 *slot = range.start + cursor + offset;
348 }
349 let next = cursor + returned;
350 Ok(Some(Q42SegmentMatchPage {
351 range,
352 returned,
353 next_cursor: (next < range.len()).then_some(next),
354 }))
355 }
356
357 pub fn segment_from_file(path: &Path, locator: String) -> io::Result<Q42VolumeSegment> {
358 let volume = Q42Volume::open(path)?;
359 let mut first = None;
360 let mut last = 0u64;
361 let mut quin_count = 0u64;
362 let mut block = [0u8; crate::q42_volume::SUPERBLOCK_SIZE];
363 for index in 0..volume.block_count() as usize {
364 volume.read_superblock_into(index, &mut block)?;
365 let live = u64::from_le_bytes(block[16..24].try_into().unwrap()) as usize;
366 for quin_index in 0..live {
367 let offset = crate::q42_volume::SUPERBLOCK_HEADER
368 + quin_index * crate::q42_volume::QUIN_SIZE;
369 let object =
370 u64::from_le_bytes(block[offset + 16..offset + 24].try_into().unwrap());
371 first.get_or_insert(object);
372 last = object;
373 quin_count += 1;
374 }
375 }
376 let Some(first_object_hash) = first else {
377 return Err(io::Error::new(
378 io::ErrorKind::InvalidInput,
379 "Q42 segment has no Quins",
380 ));
381 };
382 Ok(Q42VolumeSegment {
383 locator,
384 byte_length: std::fs::metadata(path)?.len(),
385 first_object_hash,
386 last_object_hash: last,
387 quin_count,
388 sha256: sha256_file(path)?,
389 })
390 }
391
392 pub fn lexicon_segment_from_file(
393 path: &Path,
394 locator: String,
395 ) -> io::Result<Q42LexiconSegment> {
396 let volume = Q42Volume::open(path)?;
397 let view = volume
398 .lex_view()
399 .map_err(|error| invalid(format!("invalid Q42LEX shard: {error:?}")))?;
400 let Some(first_hash) = view.hash_at(0) else {
401 return Err(invalid("Q42 lexicon shard is empty"));
402 };
403 let last_hash = view
404 .hash_at(view.entry_count() - 1)
405 .ok_or_else(|| invalid("Q42 lexicon shard has no last hash"))?;
406 Ok(Q42LexiconSegment {
407 locator,
408 byte_length: std::fs::metadata(path)?.len(),
409 first_hash,
410 last_hash,
411 sha256: sha256_file(path)?,
412 })
413 }
414}
415
416pub trait Q42SegmentRangeFactory {
419 type Source: Q42RangeSource;
420
421 fn open_segment(&self, segment: &Q42VolumeSegment) -> io::Result<Self::Source>;
422}
423
424pub trait Q42LexiconRangeFactory {
426 type Source: Q42RangeSource;
427
428 fn open_lexicon_segment(&self, segment: &Q42LexiconSegment) -> io::Result<Self::Source>;
429}
430
431impl<F, S> Q42LexiconRangeFactory for F
432where
433 F: Fn(&Q42LexiconSegment) -> io::Result<S>,
434 S: Q42RangeSource,
435{
436 type Source = S;
437
438 fn open_lexicon_segment(&self, segment: &Q42LexiconSegment) -> io::Result<Self::Source> {
439 self(segment)
440 }
441}
442
443impl<F, S> Q42SegmentRangeFactory for F
444where
445 F: Fn(&Q42VolumeSegment) -> io::Result<S>,
446 S: Q42RangeSource,
447{
448 type Source = S;
449
450 fn open_segment(&self, segment: &Q42VolumeSegment) -> io::Result<Self::Source> {
451 self(segment)
452 }
453}
454
455pub struct Q42RangeVolumeSet<S: Q42RangeSource> {
458 manifest: Q42VolumeManifest,
459 segments: Vec<Q42RangeVolume<S>>,
460 lexicon_segments: Vec<Q42RangeVolume<S>>,
461}
462
463#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
465pub struct Q42VolumeSetQueryCursor {
466 pub segment_index: usize,
467 pub segment_cursor: Q42RangeQueryCursor,
468}
469
470#[derive(Clone, Copy, Debug, Eq, PartialEq)]
471pub struct Q42VolumeSetQueryPage {
472 pub returned: usize,
473 pub next_cursor: Option<Q42VolumeSetQueryCursor>,
474}
475
476impl<S: Q42RangeSource> Q42RangeVolumeSet<S> {
477 pub fn open_root_with_lexicon_factory<R, DF, LF>(
480 root: &Q42RangeVolume<R>,
481 data_factory: &DF,
482 lexicon_factory: &LF,
483 ) -> io::Result<Self>
484 where
485 R: Q42RangeSource,
486 DF: Q42SegmentRangeFactory<Source = S>,
487 LF: Q42LexiconRangeFactory<Source = S>,
488 {
489 let mut set = Self::open_root(root, data_factory)?;
490 set.attach_lexicon_segments(&|segment| lexicon_factory.open_lexicon_segment(segment))?;
491 Ok(set)
492 }
493 pub fn open_root<R, F>(root: &Q42RangeVolume<R>, factory: &F) -> io::Result<Self>
494 where
495 R: Q42RangeSource,
496 F: Q42SegmentRangeFactory<Source = S>,
497 {
498 let manifest_length = root
499 .volume_manifest_length()?
500 .ok_or_else(|| invalid("Q42 root has no embedded volume manifest"))?;
501 let mut bytes = vec![0u8; manifest_length];
502 root.read_volume_manifest_into(&mut bytes)?;
503 let manifest = Q42VolumeManifest::decode(&bytes)?;
504 let mut segments = Vec::with_capacity(manifest.segments.len());
505 for entry in &manifest.segments {
506 let source = factory.open_segment(entry)?;
507 if source.length()? != entry.byte_length {
508 return Err(invalid(format!(
509 "Q42 segment length differs from root manifest: {}",
510 entry.locator
511 )));
512 }
513 let volume = Q42RangeVolume::open(source)?;
514 if volume.object_hash_bounds()?
515 != Some((entry.first_object_hash, entry.last_object_hash))
516 {
517 return Err(invalid(format!(
518 "Q42 segment object bounds differ from root manifest: {}",
519 entry.locator
520 )));
521 }
522 segments.push(volume);
523 }
524 Ok(Self {
525 manifest,
526 segments,
527 lexicon_segments: Vec::new(),
528 })
529 }
530
531 pub fn manifest(&self) -> &Q42VolumeManifest {
532 &self.manifest
533 }
534
535 pub fn segments(&self) -> &[Q42RangeVolume<S>] {
536 &self.segments
537 }
538
539 pub fn lexicon_segments(&self) -> &[Q42RangeVolume<S>] {
540 &self.lexicon_segments
541 }
542
543 pub fn attach_lexicon_segments<F>(&mut self, factory: &F) -> io::Result<()>
548 where
549 F: Fn(&Q42LexiconSegment) -> io::Result<S>,
550 {
551 if !self.lexicon_segments.is_empty() {
552 return Err(invalid("Q42 lexicon shards are already attached"));
553 }
554 let mut opened = Vec::with_capacity(self.manifest.lexicon_segments.len());
555 for entry in &self.manifest.lexicon_segments {
556 let source = factory(entry)?;
557 if source.length()? != entry.byte_length {
558 return Err(invalid(format!(
559 "Q42 lexicon segment length differs from root manifest: {}",
560 entry.locator
561 )));
562 }
563 let volume = Q42RangeVolume::open(source)?;
564 let mut header = [0u8; crate::q42_lex::LEX_HEADER_SIZE];
565 volume.read_lexicon_prefix_into(&mut header)?;
566 let entries = usize::try_from(u64::from_le_bytes(header[8..16].try_into().unwrap()))
567 .map_err(|_| invalid("Q42 lexicon entry count exceeds platform"))?;
568 if entries == 0 {
569 return Err(invalid("Q42 lexicon shard is empty"));
570 }
571 opened.push(volume);
572 }
573 self.lexicon_segments = opened;
574 Ok(())
575 }
576
577 pub fn lookup_lexicon_hash_into(
580 &self,
581 hash: u64,
582 page: &mut [u8],
583 out: &mut [u8],
584 ) -> io::Result<Option<usize>> {
585 if self.manifest.lexicon_segments.is_empty() {
586 return Ok(None);
587 }
588 if self.lexicon_segments.len() != self.manifest.lexicon_segments.len() {
589 return Err(invalid("Q42 lexicon shards have not been attached"));
590 }
591 let mut lo = 0usize;
592 let mut hi = self.manifest.lexicon_segments.len();
593 while lo < hi {
594 let mid = lo + (hi - lo) / 2;
595 if self.manifest.lexicon_segments[mid].first_hash <= hash {
596 lo = mid + 1;
597 } else {
598 hi = mid;
599 }
600 }
601 let Some(index) = lo.checked_sub(1) else {
602 return Ok(None);
603 };
604 if hash > self.manifest.lexicon_segments[index].last_hash {
605 return Ok(None);
606 }
607 self.lexicon_segments[index].lookup_lexicon_hash_into(hash, page, out)
608 }
609
610 pub fn execute_query_page_into(
615 &self,
616 plan: Q42RangeQueryPlan,
617 cursor: Q42VolumeSetQueryCursor,
618 compressed: &mut [u8],
619 decoded: &mut [u8],
620 out: &mut [crate::NQuin],
621 ) -> io::Result<Q42VolumeSetQueryPage> {
622 if out.is_empty() {
623 return Err(io::Error::new(
624 io::ErrorKind::InvalidInput,
625 "Q42 volume-set query output buffer is empty",
626 ));
627 }
628 let (start, end) = if let Some(object) = plan.pattern.object {
629 match self.manifest.segment_range_for_object(object) {
630 Some(range) => (range.start, range.end),
631 None => {
632 return Ok(Q42VolumeSetQueryPage {
633 returned: 0,
634 next_cursor: None,
635 })
636 }
637 }
638 } else {
639 (0, self.segments.len())
640 };
641 let mut segment_index = cursor.segment_index.max(start);
642 let mut segment_cursor = if segment_index == cursor.segment_index {
643 cursor.segment_cursor
644 } else {
645 Q42RangeQueryCursor::default()
646 };
647 let mut returned = 0usize;
648 while segment_index < end {
649 let page: Q42RangeQueryPage = self.segments[segment_index].execute_query_page_into(
650 plan,
651 segment_cursor,
652 compressed,
653 decoded,
654 &mut out[returned..],
655 )?;
656 returned += page.returned;
657 if returned == out.len() {
658 let next_cursor = match page.next_cursor {
659 Some(next) => Some(Q42VolumeSetQueryCursor {
660 segment_index,
661 segment_cursor: next,
662 }),
663 None if segment_index + 1 < end => Some(Q42VolumeSetQueryCursor {
664 segment_index: segment_index + 1,
665 segment_cursor: Q42RangeQueryCursor::default(),
666 }),
667 None => None,
668 };
669 return Ok(Q42VolumeSetQueryPage {
670 returned,
671 next_cursor,
672 });
673 }
674 if let Some(next) = page.next_cursor {
675 return Ok(Q42VolumeSetQueryPage {
676 returned,
677 next_cursor: Some(Q42VolumeSetQueryCursor {
678 segment_index,
679 segment_cursor: next,
680 }),
681 });
682 }
683 segment_index += 1;
684 segment_cursor = Q42RangeQueryCursor::default();
685 }
686 Ok(Q42VolumeSetQueryPage {
687 returned,
688 next_cursor: None,
689 })
690 }
691
692 pub fn segment_index_for_object(&self, object_hash: u64) -> Option<usize> {
696 self.manifest
697 .segment_range_for_object(object_hash)
698 .map(|range| range.start)
699 }
700
701 pub fn segment_indices_for_object_into(
702 &self,
703 object_hash: u64,
704 cursor: usize,
705 out: &mut [usize],
706 ) -> io::Result<Option<Q42SegmentMatchPage>> {
707 self.manifest
708 .segment_indices_for_object_into(object_hash, cursor, out)
709 }
710
711 pub fn verify_segment_hashes(&self, scratch: &mut [u8]) -> io::Result<()> {
713 for (entry, segment) in self.manifest.segments.iter().zip(&self.segments) {
714 verify_source_sha256(segment.source(), &entry.sha256, scratch)?;
715 }
716 Ok(())
717 }
718
719 pub fn verify_segment_quin_counts(
722 &self,
723 compressed: &mut [u8],
724 decoded: &mut [u8],
725 ) -> io::Result<()> {
726 if compressed.len() < MAX_COMPRESSED_SUPERBLOCK_SIZE || decoded.len() < SUPERBLOCK_SIZE {
727 return Err(io::Error::new(
728 io::ErrorKind::InvalidInput,
729 "Q42 segment verifier buffers are too small",
730 ));
731 }
732 for (entry, segment) in self.manifest.segments.iter().zip(&self.segments) {
733 let mut actual = 0u64;
734 for index in 0..segment.block_count() as usize {
735 segment.read_superblock_into(index, compressed, decoded)?;
736 let live = u64::from_le_bytes(decoded[16..24].try_into().unwrap());
737 if live > crate::QUINS_PER_BLOCK as u64 {
738 return Err(invalid("Q42 SuperBlock exceeds its Quin capacity"));
739 }
740 actual = actual
741 .checked_add(live)
742 .ok_or_else(|| invalid("Q42 segment Quin count overflow"))?;
743 }
744 if actual != entry.quin_count {
745 return Err(invalid(format!(
746 "Q42 segment Quin count differs from root manifest: {}",
747 entry.locator
748 )));
749 }
750 }
751 Ok(())
752 }
753}
754
755impl Q42VolumeSegment {
756 pub fn ipfs_cid(&self) -> Option<&str> {
758 self.locator.strip_prefix("ipfs://")
759 }
760}
761
762fn validate_segment_locator(locator: &str) -> io::Result<()> {
763 if locator.is_empty() {
764 return Err(io::Error::new(
765 io::ErrorKind::InvalidInput,
766 "Q42 segment locator is empty",
767 ));
768 }
769 if let Some(cid) = locator.strip_prefix("ipfs://") {
770 if cid.is_empty() || !cid.bytes().all(|byte| byte.is_ascii_alphanumeric()) {
771 return Err(io::Error::new(
772 io::ErrorKind::InvalidInput,
773 "Q42 IPFS locator has an invalid CID",
774 ));
775 }
776 return Ok(());
777 }
778 let path = Path::new(locator);
779 if path.is_absolute()
780 || path.components().any(|component| {
781 matches!(
782 component,
783 Component::ParentDir | Component::RootDir | Component::Prefix(_)
784 )
785 })
786 {
787 return Err(io::Error::new(
788 io::ErrorKind::InvalidInput,
789 "Q42 segment locator must be a relative path or ipfs://CID",
790 ));
791 }
792 Ok(())
793}
794
795pub struct Q42VolumeSet {
798 root: Q42Volume,
799 manifest: Q42VolumeManifest,
800 segments: Vec<Q42Volume>,
801 lexicon_segments: Vec<Q42Volume>,
802}
803
804impl Q42VolumeSet {
805 pub fn open_root(path: &Path) -> io::Result<Self> {
806 let root = Q42Volume::open(path)?;
807 let manifest = root
808 .volume_manifest()?
809 .ok_or_else(|| invalid("Q42 root has no embedded volume manifest"))?;
810 let parent = path.parent().unwrap_or_else(|| Path::new("."));
811 let mut segments = Vec::with_capacity(manifest.segments.len());
812 for entry in &manifest.segments {
813 let segment_path = parent.join(&entry.locator);
814 if std::fs::metadata(&segment_path)?.len() != entry.byte_length {
815 return Err(invalid(format!(
816 "Q42 segment length differs from root manifest: {}",
817 entry.locator
818 )));
819 }
820 let segment = Q42Volume::open(&segment_path)?;
821 if segment.object_hash_bounds()
822 != Some((entry.first_object_hash, entry.last_object_hash))
823 {
824 return Err(invalid(format!(
825 "Q42 segment object bounds differ from root manifest: {}",
826 entry.locator
827 )));
828 }
829 segments.push(segment);
830 }
831 let mut lexicon_segments = Vec::with_capacity(manifest.lexicon_segments.len());
832 for entry in &manifest.lexicon_segments {
833 let segment_path = parent.join(&entry.locator);
834 if std::fs::metadata(&segment_path)?.len() != entry.byte_length {
835 return Err(invalid(format!(
836 "Q42 lexicon segment length differs from root manifest: {}",
837 entry.locator
838 )));
839 }
840 let segment = Q42Volume::open(&segment_path)?;
841 let view = segment
842 .lex_view()
843 .map_err(|error| invalid(format!("invalid Q42 lexicon segment: {error:?}")))?;
844 if view.hash_at(0) != Some(entry.first_hash)
845 || view.hash_at(view.entry_count().saturating_sub(1)) != Some(entry.last_hash)
846 {
847 return Err(invalid(format!(
848 "Q42 lexicon segment hash bounds differ from root manifest: {}",
849 entry.locator
850 )));
851 }
852 lexicon_segments.push(segment);
853 }
854 Ok(Self {
855 root,
856 manifest,
857 segments,
858 lexicon_segments,
859 })
860 }
861
862 pub fn root(&self) -> &Q42Volume {
864 &self.root
865 }
866
867 pub fn manifest(&self) -> &Q42VolumeManifest {
868 &self.manifest
869 }
870 pub fn segments(&self) -> &[Q42Volume] {
871 &self.segments
872 }
873
874 pub fn lexicon_segments(&self) -> &[Q42Volume] {
875 &self.lexicon_segments
876 }
877
878 pub fn lookup_hash(&self, hash: u64) -> Option<&str> {
882 if let Ok(root) = self.root.lex_view() {
883 if let Some(value) = root.lookup_hash(hash) {
884 return Some(value);
885 }
886 }
887 let mut lo = 0usize;
888 let mut hi = self.manifest.lexicon_segments.len();
889 while lo < hi {
890 let mid = lo + (hi - lo) / 2;
891 if self.manifest.lexicon_segments[mid].first_hash <= hash {
892 lo = mid + 1;
893 } else {
894 hi = mid;
895 }
896 }
897 let index = lo.checked_sub(1)?;
898 let descriptor = self.manifest.lexicon_segments.get(index)?;
899 if hash > descriptor.last_hash {
900 return None;
901 }
902 self.lexicon_segments
903 .get(index)?
904 .lex_view()
905 .ok()?
906 .lookup_hash(hash)
907 }
908
909 pub fn verify_segment_hashes(&self, root_path: &Path) -> io::Result<()> {
910 let parent = root_path.parent().unwrap_or_else(|| Path::new("."));
911 for entry in &self.manifest.segments {
912 if sha256_file(&parent.join(&entry.locator))? != entry.sha256 {
913 return Err(invalid(format!(
914 "Q42 segment digest differs from root manifest: {}",
915 entry.locator
916 )));
917 }
918 }
919 for entry in &self.manifest.lexicon_segments {
920 if sha256_file(&parent.join(&entry.locator))? != entry.sha256 {
921 return Err(invalid(format!(
922 "Q42 lexicon segment digest differs from root manifest: {}",
923 entry.locator
924 )));
925 }
926 }
927 Ok(())
928 }
929
930 pub fn verify_all(&self, root_path: &Path) -> io::Result<Q42VerificationReceipt> {
934 self.verify_segment_hashes(root_path)?;
935 let mut receipt = Q42VerificationReceipt {
936 blocks_verified: 0,
937 quins_verified: 0,
938 };
939 for (entry, segment) in self.manifest.segments.iter().zip(&self.segments) {
940 let segment_receipt = segment.verify_all_blocks()?;
941 if segment_receipt.quins_verified != entry.quin_count {
942 return Err(invalid(format!(
943 "Q42 verified Quin count differs from root manifest: {}",
944 entry.locator
945 )));
946 }
947 receipt.blocks_verified = receipt
948 .blocks_verified
949 .checked_add(segment_receipt.blocks_verified)
950 .ok_or_else(|| invalid("Q42 verified block count overflows"))?;
951 receipt.quins_verified = receipt
952 .quins_verified
953 .checked_add(segment_receipt.quins_verified)
954 .ok_or_else(|| invalid("Q42 verified Quin count overflows"))?;
955 }
956 Ok(receipt)
957 }
958}
959
960fn sha256_file(path: &Path) -> io::Result<[u8; 32]> {
961 let mut file = File::open(path)?;
962 let mut hasher = Sha256::new();
963 let mut buffer = [0u8; 64 * 1024];
964 loop {
965 let read = file.read(&mut buffer)?;
966 if read == 0 {
967 break;
968 }
969 hasher.update(&buffer[..read]);
970 }
971 Ok(hasher.finalize().into())
972}
973
974pub fn root_relative_path(root: &Path, segment: &Path) -> io::Result<String> {
975 let parent = root.parent().unwrap_or_else(|| Path::new("."));
976 let relative: PathBuf = segment
977 .strip_prefix(parent)
978 .map_err(|_| {
979 io::Error::new(
980 io::ErrorKind::InvalidInput,
981 "segment must be below the root Q42 directory",
982 )
983 })?
984 .to_owned();
985 relative.to_str().map(str::to_owned).ok_or_else(|| {
986 io::Error::new(
987 io::ErrorKind::InvalidInput,
988 "segment path is not valid UTF-8",
989 )
990 })
991}
992
993#[cfg(test)]
994mod tests {
995 use super::*;
996
997 fn segment(locator: &str) -> Q42VolumeSegment {
998 Q42VolumeSegment {
999 locator: locator.to_owned(),
1000 byte_length: 1,
1001 first_object_hash: 1,
1002 last_object_hash: 1,
1003 quin_count: 1,
1004 sha256: [7; 32],
1005 }
1006 }
1007
1008 #[test]
1009 fn manifest_accepts_immutable_ipfs_cids_and_rejects_escaping_paths() {
1010 let manifest = Q42VolumeManifest {
1011 generation: 1,
1012 segments: vec![segment("ipfs://bafybeigdyrzt5v5cbe")],
1013 lexicon_segments: vec![],
1014 };
1015 let bytes = manifest.encode().unwrap();
1016 assert_eq!(Q42VolumeManifest::decode(&bytes).unwrap(), manifest);
1017 assert_eq!(manifest.segments[0].ipfs_cid(), Some("bafybeigdyrzt5v5cbe"));
1018
1019 for locator in [
1020 "../segment.q42",
1021 "C:\\segment.q42",
1022 "/segment.q42",
1023 "ipfs://bad/path",
1024 ] {
1025 let invalid = Q42VolumeManifest {
1026 generation: 1,
1027 segments: vec![segment(locator)],
1028 lexicon_segments: vec![],
1029 };
1030 assert!(
1031 invalid.validate().is_err(),
1032 "locator {locator:?} must be rejected"
1033 );
1034 }
1035 }
1036
1037 #[test]
1038 fn manifest_pages_all_boundary_spanning_segments_without_allocation() {
1039 let mut first = segment("one.q42");
1040 let mut second = segment("two.q42");
1041 let mut third = segment("three.q42");
1042 first.first_object_hash = 41;
1043 first.last_object_hash = 42;
1044 second.first_object_hash = 42;
1045 second.last_object_hash = 42;
1046 third.first_object_hash = 42;
1047 third.last_object_hash = 43;
1048 let manifest = Q42VolumeManifest {
1049 generation: 1,
1050 segments: vec![first, second, third],
1051 lexicon_segments: vec![],
1052 };
1053 manifest.validate().unwrap();
1054 let mut page = [usize::MAX; 2];
1055 let first_page = manifest
1056 .segment_indices_for_object_into(42, 0, &mut page)
1057 .unwrap()
1058 .unwrap();
1059 assert_eq!(first_page.range, Q42SegmentMatchRange { start: 0, end: 3 });
1060 assert_eq!(&page, &[0, 1]);
1061 assert_eq!(first_page.next_cursor, Some(2));
1062 let second_page = manifest
1063 .segment_indices_for_object_into(42, 2, &mut page)
1064 .unwrap()
1065 .unwrap();
1066 assert_eq!(second_page.returned, 1);
1067 assert_eq!(page[0], 2);
1068 assert_eq!(second_page.next_cursor, None);
1069 }
1070}