Skip to main content

qualia_core_db/q42/volume/
manifest.rs

1//! Embedded root-volume descriptor for a logical multi-segment Q42 dataset.
2
3use 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/// One immutable child segment in a logical Q42 volume.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct Q42VolumeSegment {
32    /// A root-relative local path today; a future range source will also accept
33    /// immutable content-addressed locators such as `ipfs://<cid>`.
34    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/// Immutable physical Q42 file holding a contiguous hash range of the shared
43/// Q42LEX dictionary.  It has no graph blocks: terms remain front-embedded in
44/// a Q42 container and can be retrieved by exact ranges independently of data
45/// segment size.
46#[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/// Front-embedded catalog which makes immutable Q42 segments one snapshot.
56#[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/// A contiguous interval of volume segments that can contain one object hash.
64#[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/// One caller-buffered page from a manifest object-hash segment match.
81#[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    /// Return every segment whose committed object interval can contain the
293    /// given value. Equal boundaries remain complete across adjacent volumes.
294    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    /// Fill a bounded page of matching segment indices. This caps caller work
323    /// even when a high-frequency object falls on many volume boundaries.
324    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
416/// Opens an immutable child source identified by the front-embedded manifest.
417/// Construction may allocate; all subsequent block reads remain caller-buffered.
418pub trait Q42SegmentRangeFactory {
419    type Source: Q42RangeSource;
420
421    fn open_segment(&self, segment: &Q42VolumeSegment) -> io::Result<Self::Source>;
422}
423
424/// Opens a manifest-attested Q42LEX shard through a range transport.
425pub 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
455/// Transport-neutral logical Q42 snapshot. Root and child volumes can be
456/// opened from HTTP/IPFS range sources just as from a local file.
457pub struct Q42RangeVolumeSet<S: Q42RangeSource> {
458    manifest: Q42VolumeManifest,
459    segments: Vec<Q42RangeVolume<S>>,
460    lexicon_segments: Vec<Q42RangeVolume<S>>,
461}
462
463/// Resume state for a range-backed scan across a logical Q42 volume set.
464#[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    /// Open a root's graph and lexicon shards as one transport-neutral range
478    /// snapshot. This is the service-facing constructor for HTTP/IPFS callers.
479    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    /// Attach the root-manifested Q42LEX shards through the same exact-range
544    /// transport used for graph segments.  The separate closure keeps legacy
545    /// data-only factories source-compatible while allowing IPFS locators for
546    /// lexicon shards.
547    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    /// Resolve a term through the hash-routed lexicon shard.  Callers supply
578    /// page and text buffers, preserving the zero-heap range contract.
579    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    /// Scan one bounded result page across child segments.  Bound-object
611    /// patterns prune the segment list through the root manifest before the
612    /// per-segment BIDX reader runs; other patterns stream segments in their
613    /// stable manifest order.
614    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    /// Find the first segment whose object interval can contain `object_hash`.
693    /// Segment boundary overlap is legal for high-frequency values, so callers
694    /// must advance through adjoining intervals when a value spans segments.
695    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    /// Verify every immutable child digest using a caller-owned scratch buffer.
712    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    /// Verify the committed Quin count in every segment. The supplied buffers
720    /// are reused for every block and cap the verifier's memory use.
721    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    /// Return the immutable CID for an `ipfs://CID` locator.
757    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
795/// Locally backed multi-segment query snapshot. It deliberately has no remote
796/// transport yet: IPFS/HTTP range sources will implement the same contract.
797pub 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    /// The front-matter root that owns the snapshot-wide lexicon and manifest.
863    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    /// Resolve a term through the root dictionary or its front-manifested
879    /// physical lexicon shards.  Construction owns the maps/mappings; lookup
880    /// borrows directly from the selected Q42LEX page.
881    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    /// Exhaustively verify a local logical snapshot: manifest-attested child
931    /// digests plus every decoded data Quin.  This remains a cold operation;
932    /// each physical verifier uses a single fixed SuperBlock scratch buffer.
933    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}