1use std::collections::HashMap;
16use std::fs::{File, OpenOptions};
17use std::io::{self, BufWriter, Read, Write};
18use std::path::Path;
19
20use memmap2::{Mmap, MmapOptions};
21
22use crate::q42_lex::{LexError, LexiconEntry, Q42LexMmap, LEX_MAGIC};
23use crate::{NQuin, QUINS_PER_BLOCK};
24
25#[path = "volume/mod.rs"]
26mod volume;
27
28#[cfg(not(target_arch = "wasm32"))]
29pub use volume::{
30 ipfs_gateway_range_source, ipns_gateway_range_source, HttpRangeSource,
31 IpfsGatewaySegmentFactory,
32};
33pub use volume::{
34 root_relative_path, validate_exact_range_response, verify_source_sha256, BidxBlockRange,
35 BidxMatchPage, LocalFileRangeSource, Q42BlockCursor, Q42BlockMeta, Q42ByteRange,
36 Q42LexiconRangeFactory, Q42LexiconSegment, Q42ObjectMatchPage, Q42ObjectSearchCursor,
37 Q42RangeQueryCursor, Q42RangeQueryPage, Q42RangeQueryPattern, Q42RangeQueryPlan,
38 Q42RangeQueryStrategy, Q42RangeSource, Q42RangeVolume, Q42RangeVolumeSet, Q42SegmentMatchPage,
39 Q42SegmentMatchRange, Q42SegmentRangeFactory, Q42VolumeManifest, Q42VolumeSegment,
40 Q42VolumeSet, Q42VolumeSetQueryCursor, Q42VolumeSetQueryPage, StreamingQ42VolumeWriter,
41 VerifiedCarBlock, CidSha256, BlockFieldPostings, Q42InspectReport, Q42SectionReport,
42 Q42Magnet, Q42VolumeSetMagnets, Q42RolloverPublisher, Q42QueryMode, Q42VerifyReceipt,
43 Q42VerifySetReport,
44 VerifyLevel, CheckStatus, VerifyCheck, compose_magnet, sha1_hex_file,
45 classify_q42_path, classify_q42_volume, classify_q42_volume_set, deny_public_publication,
46 quin_requires_sanctuary, ClassificationCounts, PublicationIntent, Q42PublicationClass,
47 Q42PublicationVerdict, Q42Transport,
48 append_segment_to_root, verify_volume_set_from_root, DEFAULT_SEGMENT_MAX_BYTES,
49 RESIDENT_QUERY_MAX_BYTES, decode_and_verify_car, encode_raw_car,
50 extract_entity_bytes, inclusive_entity_bytes, encode_block_postings, encode_postings_section,
51 measure_bloom_false_positives, FIELD_POSTINGS_MAGIC, MAX_VOLUME_MANIFEST_BYTES,
52 compact_volume_set, verify_car_bytes_as_q42_source, verify_local_car_as_q42_source,
53 OpfsCallbackRangeSource, OpfsSliceRangeSource, VerifiedCarRangeSource,
54};
55#[cfg(not(target_arch = "wasm32"))]
56pub use volume::{write_sorted_quins_volume, write_sorted_quins_volume_with_author};
57
58pub const Q42_MAGIC: [u8; 4] = [0x51, 0x34, 0x32, 0x00]; pub const Q42_VERSION_V3: u16 = 3;
60pub const HEADER_SIZE: usize = 256;
61pub const SUPERBLOCK_SIZE: usize = 40_960;
62pub const MAX_COMPRESSED_SUPERBLOCK_SIZE: usize = SUPERBLOCK_SIZE + (SUPERBLOCK_SIZE / 255) + 36;
65pub const SUPERBLOCK_HEADER: usize = 160;
66pub const QUIN_SIZE: usize = 48;
67pub const BIDX_MAGIC: [u8; 4] = *b"BIDX";
68pub const FLAG_BLOCKS_LZ4: u16 = 0x0001;
69pub const FLAG_OBJECT_SORTED: u16 = 0x0002;
70pub const FLAG_VOLUME_ROOT: u16 = 0x0004;
71pub const FLAG_FIELD_RANGES: u16 = 0x0008;
74pub const FLAG_FIELD_POSTINGS: u16 = 0x0010;
76pub const FLAG_PERMISSIVE_COMMONS: u16 = 0x0020;
79pub const FLAG_SANCTUARY: u16 = 0x0040;
83pub const FIELD_RANGE_INDEX_MAGIC: [u8; 4] = *b"FIDX";
84pub const FIELD_RANGE_INDEX_HEADER_BYTES: usize = 16;
85pub const FIELD_RANGE_INDEX_ENTRY_BYTES: usize = 48;
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
89pub struct Q42VerificationReceipt {
90 pub blocks_verified: u64,
91 pub quins_verified: u64,
92}
93
94#[repr(C, packed)]
97#[derive(Clone, Copy, Debug)]
98pub struct Q42VolumeHeader {
99 pub magic: [u8; 4],
100 pub version: u16,
101 pub flags: u16,
102 pub lex_offset: u64,
103 pub lex_length: u64,
104 pub bidx_offset: u64,
105 pub bidx_length: u64,
106 pub block_dir_offset: u64,
107 pub block_dir_length: u64,
108 pub data_offset: u64,
109 pub data_length: u64,
110 pub block_count: u64,
111 pub block_size: u32,
112 pub quins_per_block: u32,
113 pub temporal_index_offset: u64,
115 pub temporal_index_length: u64,
116 pub merkle_root: [u8; 32], pub assertion_timestamp: u64, pub dag_root_offset: u64, pub dag_root_length: u64, pub natural_person_did_offset: u64, pub software_agent_did_offset: u64, pub _reserved: [u8; 80], }
127
128const _: () = assert!(
129 std::mem::size_of::<Q42VolumeHeader>() == 256,
130 "Q42VolumeHeader must be exactly 256 bytes — matches HEADER_SIZE constant"
131);
132
133impl Q42VolumeHeader {
134 pub fn verify_version(&self) -> Result<(), String> {
136 let magic = self.magic;
138 let version = { self.version };
139 if magic != Q42_MAGIC {
140 return Err(format!("bad magic {magic:?}"));
141 }
142 if version != Q42_VERSION_V3 {
143 return Err(format!("Q42 file is version {version}; strict v3 required"));
144 }
145 Ok(())
146 }
147
148 fn volume_manifest_range(&self) -> Option<(u64, u64)> {
149 if self.flags & FLAG_VOLUME_ROOT == 0 {
150 return None;
151 }
152 let reserved = self._reserved;
153 Some((
154 u64::from_le_bytes(reserved[0..8].try_into().unwrap()),
155 u64::from_le_bytes(reserved[8..16].try_into().unwrap()),
156 ))
157 }
158
159 fn field_range_index_range(&self) -> Option<(u64, u64)> {
160 if self.flags & FLAG_FIELD_RANGES == 0 {
161 return None;
162 }
163 let reserved = self._reserved;
164 Some((
165 u64::from_le_bytes(reserved[16..24].try_into().unwrap()),
166 u64::from_le_bytes(reserved[24..32].try_into().unwrap()),
167 ))
168 }
169
170 fn field_postings_range(&self) -> Option<(u64, u64)> {
171 if self.flags & FLAG_FIELD_POSTINGS == 0 {
172 return None;
173 }
174 let reserved = self._reserved;
175 Some((
176 u64::from_le_bytes(reserved[32..40].try_into().unwrap()),
177 u64::from_le_bytes(reserved[40..48].try_into().unwrap()),
178 ))
179 }
180
181 pub fn new_v3(
183 lex_offset: u64,
184 lex_length: u64,
185 bidx_offset: u64,
186 bidx_length: u64,
187 block_dir_offset: u64,
188 block_dir_length: u64,
189 data_offset: u64,
190 data_length: u64,
191 block_count: u64,
192 block_size: u32,
193 quins_per_block: u32,
194 ) -> Self {
195 let assertion_timestamp = std::time::SystemTime::now()
196 .duration_since(std::time::UNIX_EPOCH)
197 .map(|d| d.as_millis() as u64)
198 .unwrap_or(0);
199 Self {
200 magic: Q42_MAGIC,
201 version: Q42_VERSION_V3,
202 flags: FLAG_BLOCKS_LZ4 | FLAG_OBJECT_SORTED,
203 lex_offset,
204 lex_length,
205 bidx_offset,
206 bidx_length,
207 block_dir_offset,
208 block_dir_length,
209 data_offset,
210 data_length,
211 block_count,
212 block_size,
213 quins_per_block,
214 temporal_index_offset: 0,
215 temporal_index_length: 0,
216 merkle_root: [0u8; 32],
217 assertion_timestamp,
218 dag_root_offset: 0,
219 dag_root_length: 0,
220 natural_person_did_offset: 0,
221 software_agent_did_offset: 0,
222 _reserved: [0u8; 80],
223 }
224 }
225}
226
227#[repr(C, packed)]
228#[derive(Clone, Copy, Debug)]
229pub struct BlockDirectoryEntry {
230 pub rel_offset: u64,
231 pub comp_len: u32,
232 pub uncomp_len: u32,
233}
234
235impl BlockDirectoryEntry {
236 pub const SIZE: usize = 16;
237
238 pub fn write_to(&self, out: &mut impl Write) -> io::Result<()> {
239 out.write_all(&self.rel_offset.to_le_bytes())?;
240 out.write_all(&self.comp_len.to_le_bytes())?;
241 out.write_all(&self.uncomp_len.to_le_bytes())?;
242 Ok(())
243 }
244
245 fn from_bytes(buf: &[u8; 16]) -> Self {
246 Self {
247 rel_offset: u64::from_le_bytes(buf[0..8].try_into().unwrap()),
248 comp_len: u32::from_le_bytes(buf[8..12].try_into().unwrap()),
249 uncomp_len: u32::from_le_bytes(buf[12..16].try_into().unwrap()),
250 }
251 }
252}
253
254pub fn migrate_v2_to_v3(path: &Path) -> io::Result<()> {
257 use std::io::{Seek, SeekFrom};
258 let mut f = OpenOptions::new().read(true).write(true).open(path)?;
259 let mut header = [0u8; HEADER_SIZE];
260 f.read_exact(&mut header)?;
261 if &header[0..4] != &Q42_MAGIC {
262 return Err(io::Error::new(
263 io::ErrorKind::InvalidData,
264 "not a Q42 volume",
265 ));
266 }
267 let version = u16::from_le_bytes([header[4], header[5]]);
268 if version >= Q42_VERSION_V3 as u16 {
269 return Ok(()); }
271 header[4..6].copy_from_slice(&(Q42_VERSION_V3 as u16).to_le_bytes());
273 let flags = u16::from_le_bytes([header[6], header[7]]) & (FLAG_BLOCKS_LZ4 | FLAG_OBJECT_SORTED);
276 header[6..8].copy_from_slice(&flags.to_le_bytes());
277 header[88..256].fill(0);
279 f.seek(SeekFrom::Start(0))?;
280 f.write_all(&header)?;
281 f.flush()
282}
283
284pub fn is_unified_volume(path: &Path) -> io::Result<bool> {
286 let mut f = File::open(path)?;
287 let mut magic = [0u8; 4];
288 f.read_exact(&mut magic)?;
289 Ok(magic == Q42_MAGIC)
290}
291
292pub fn encode_lex(lex: &HashMap<u64, String>) -> Result<Vec<u8>, LexError> {
294 crate::q42_lex::serialize_paged_string_lexicon(lex, crate::q42_lex::DEFAULT_LEX_PAGE_ENTRIES)
299}
300
301pub fn encode_lex_with_entries(lex: &HashMap<u64, LexiconEntry>) -> Result<Vec<u8>, LexError> {
303 let mut entries: Vec<(u64, &LexiconEntry)> = lex.iter().map(|(&h, e)| (h, e)).collect();
304 entries.sort_unstable_by_key(|&(h, _)| h);
305
306 let entry_count = entries.len() as u64;
307 let strings_offset = 32 + entry_count * 16;
308
309 let mut string_blob: Vec<u8> = Vec::new();
310 let mut index = Vec::with_capacity(entries.len() * 16);
311 for (hash, entry) in &entries {
312 let str_off = string_blob.len() as u64;
313 match entry {
314 LexiconEntry::String(text) => {
315 string_blob.push(0x01);
317 let b = text.as_bytes();
318 let len = u16::try_from(b.len()).map_err(|_| LexError::TermTooLong)?;
319 string_blob.extend_from_slice(&len.to_le_bytes());
320 string_blob.extend_from_slice(&b[..len as usize]);
321 }
322 LexiconEntry::EmbeddedTriple(triple) => {
323 string_blob.push(0x02);
325 for &id in triple {
326 string_blob.extend_from_slice(&id.to_le_bytes());
327 }
328 }
329 LexiconEntry::Webizen(webid) => {
330 string_blob.push(0x03);
332 let b = webid.as_bytes();
333 let len = u16::try_from(b.len()).map_err(|_| LexError::TermTooLong)?;
334 string_blob.extend_from_slice(&len.to_le_bytes());
335 string_blob.extend_from_slice(&b[..len as usize]);
336 }
337 }
338 index.extend_from_slice(&hash.to_le_bytes());
339 index.extend_from_slice(&str_off.to_le_bytes());
340 }
341
342 let mut out = Vec::with_capacity(strings_offset as usize + string_blob.len());
343 out.extend_from_slice(&LEX_MAGIC);
344 out.extend_from_slice(&entry_count.to_le_bytes());
345 out.extend_from_slice(&strings_offset.to_le_bytes());
346 out.extend_from_slice(&1u64.to_le_bytes());
347 out.extend_from_slice(&index);
348 out.extend_from_slice(&string_blob);
349 Ok(out)
350}
351
352pub fn encode_bidx(ranges: &[(u64, u64)]) -> Vec<u8> {
354 let block_count = ranges.len() as u32;
355 let mut out = Vec::with_capacity(16 + ranges.len() * 16);
356 out.extend_from_slice(&BIDX_MAGIC);
357 out.extend_from_slice(&1u32.to_le_bytes());
358 out.extend_from_slice(&block_count.to_le_bytes());
359 out.extend_from_slice(&0u32.to_le_bytes());
360 for (min, max) in ranges {
361 out.extend_from_slice(&min.to_le_bytes());
362 out.extend_from_slice(&max.to_le_bytes());
363 }
364 out
365}
366
367pub fn encode_field_range_index(ranges: &[(u64, u64, u64, u64, u64, u64)]) -> Vec<u8> {
371 let mut out = Vec::with_capacity(
372 FIELD_RANGE_INDEX_HEADER_BYTES + ranges.len() * FIELD_RANGE_INDEX_ENTRY_BYTES,
373 );
374 out.extend_from_slice(&FIELD_RANGE_INDEX_MAGIC);
375 out.extend_from_slice(&1u32.to_le_bytes());
376 out.extend_from_slice(&(ranges.len() as u32).to_le_bytes());
377 out.extend_from_slice(&0u32.to_le_bytes());
378 for range in ranges {
379 for value in [range.0, range.1, range.2, range.3, range.4, range.5] {
380 out.extend_from_slice(&value.to_le_bytes());
381 }
382 }
383 out
384}
385
386pub fn encode_superblock(seq_id: u64, quins: &[NQuin]) -> [u8; SUPERBLOCK_SIZE] {
388 debug_assert!(quins.len() <= QUINS_PER_BLOCK);
389 let mut block = [0u8; SUPERBLOCK_SIZE];
390 block[0..8].copy_from_slice(&seq_id.to_le_bytes());
391 block[16..24].copy_from_slice(&(quins.len() as u64).to_le_bytes());
392 let zero = [0u8; QUIN_SIZE];
393 let mut off = SUPERBLOCK_HEADER;
394 for q in quins {
395 block[off..off + QUIN_SIZE].copy_from_slice(bytemuck::bytes_of(q));
396 off += QUIN_SIZE;
397 }
398 for _ in quins.len()..QUINS_PER_BLOCK {
399 block[off..off + QUIN_SIZE].copy_from_slice(&zero);
400 off += QUIN_SIZE;
401 }
402 block
403}
404
405pub fn decode_superblock_quins(block: &[u8]) -> io::Result<Vec<NQuin>> {
407 if block.len() < SUPERBLOCK_HEADER {
408 return Err(io::Error::new(
409 io::ErrorKind::InvalidData,
410 "SuperBlock shorter than its 160-byte header",
411 ));
412 }
413 let live = u64::from_le_bytes(block[16..24].try_into().unwrap()) as usize;
414 if live > QUINS_PER_BLOCK {
415 return Err(io::Error::new(
416 io::ErrorKind::InvalidData,
417 "SuperBlock live Quin count exceeds capacity",
418 ));
419 }
420 let mut out = Vec::with_capacity(live);
421 for index in 0..live {
422 let offset = SUPERBLOCK_HEADER + index * QUIN_SIZE;
423 let end = offset + QUIN_SIZE;
424 if end > block.len() {
425 return Err(io::Error::new(
426 io::ErrorKind::InvalidData,
427 "SuperBlock truncated inside a Quin slot",
428 ));
429 }
430 out.push(bytemuck::pod_read_unaligned(&block[offset..end]));
431 }
432 Ok(out)
433}
434
435pub fn header_to_bytes(h: &Q42VolumeHeader) -> [u8; HEADER_SIZE] {
436 let mut buf = [0u8; HEADER_SIZE];
437 buf[0..4].copy_from_slice(&h.magic);
439 buf[4..6].copy_from_slice(&h.version.to_le_bytes());
440 buf[6..8].copy_from_slice(&h.flags.to_le_bytes());
441 buf[8..16].copy_from_slice(&h.lex_offset.to_le_bytes());
442 buf[16..24].copy_from_slice(&h.lex_length.to_le_bytes());
443 buf[24..32].copy_from_slice(&h.bidx_offset.to_le_bytes());
444 buf[32..40].copy_from_slice(&h.bidx_length.to_le_bytes());
445 buf[40..48].copy_from_slice(&h.block_dir_offset.to_le_bytes());
446 buf[48..56].copy_from_slice(&h.block_dir_length.to_le_bytes());
447 buf[56..64].copy_from_slice(&h.data_offset.to_le_bytes());
448 buf[64..72].copy_from_slice(&h.data_length.to_le_bytes());
449 buf[72..80].copy_from_slice(&h.block_count.to_le_bytes());
450 buf[80..84].copy_from_slice(&h.block_size.to_le_bytes());
451 buf[84..88].copy_from_slice(&h.quins_per_block.to_le_bytes());
452 buf[88..96].copy_from_slice(&h.temporal_index_offset.to_le_bytes());
454 buf[96..104].copy_from_slice(&h.temporal_index_length.to_le_bytes());
455 buf[104..136].copy_from_slice(&h.merkle_root);
456 buf[136..144].copy_from_slice(&h.assertion_timestamp.to_le_bytes());
457 buf[144..152].copy_from_slice(&h.dag_root_offset.to_le_bytes());
458 buf[152..160].copy_from_slice(&h.dag_root_length.to_le_bytes());
459 buf[160..168].copy_from_slice(&h.natural_person_did_offset.to_le_bytes());
460 buf[168..176].copy_from_slice(&h.software_agent_did_offset.to_le_bytes());
461 buf[176..256].copy_from_slice(&h._reserved);
462 buf
463}
464
465fn header_from_bytes(buf: &[u8; HEADER_SIZE]) -> io::Result<Q42VolumeHeader> {
466 if buf[0..4] != Q42_MAGIC {
467 return Err(io::Error::new(
468 io::ErrorKind::InvalidData,
469 "invalid Q42 magic",
470 ));
471 }
472 let version = u16::from_le_bytes(buf[4..6].try_into().unwrap());
473 if version != Q42_VERSION_V3 {
474 return Err(io::Error::new(
475 io::ErrorKind::InvalidData,
476 format!("Q42 file is version {version}; strict v3 required"),
477 ));
478 }
479 Ok(Q42VolumeHeader {
480 magic: Q42_MAGIC,
481 version,
482 flags: u16::from_le_bytes(buf[6..8].try_into().unwrap()),
483 lex_offset: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
484 lex_length: u64::from_le_bytes(buf[16..24].try_into().unwrap()),
485 bidx_offset: u64::from_le_bytes(buf[24..32].try_into().unwrap()),
486 bidx_length: u64::from_le_bytes(buf[32..40].try_into().unwrap()),
487 block_dir_offset: u64::from_le_bytes(buf[40..48].try_into().unwrap()),
488 block_dir_length: u64::from_le_bytes(buf[48..56].try_into().unwrap()),
489 data_offset: u64::from_le_bytes(buf[56..64].try_into().unwrap()),
490 data_length: u64::from_le_bytes(buf[64..72].try_into().unwrap()),
491 block_count: u64::from_le_bytes(buf[72..80].try_into().unwrap()),
492 block_size: u32::from_le_bytes(buf[80..84].try_into().unwrap()),
493 quins_per_block: u32::from_le_bytes(buf[84..88].try_into().unwrap()),
494 temporal_index_offset: u64::from_le_bytes(buf[88..96].try_into().unwrap()),
495 temporal_index_length: u64::from_le_bytes(buf[96..104].try_into().unwrap()),
496 merkle_root: buf[104..136].try_into().unwrap(),
497 assertion_timestamp: u64::from_le_bytes(buf[136..144].try_into().unwrap()),
498 dag_root_offset: u64::from_le_bytes(buf[144..152].try_into().unwrap()),
499 dag_root_length: u64::from_le_bytes(buf[152..160].try_into().unwrap()),
500 natural_person_did_offset: u64::from_le_bytes(buf[160..168].try_into().unwrap()),
501 software_agent_did_offset: u64::from_le_bytes(buf[168..176].try_into().unwrap()),
502 _reserved: buf[176..256].try_into().unwrap(),
503 })
504}
505
506pub fn write_unified_volume(
508 path: &Path,
509 lex: &HashMap<u64, String>,
510 block_ranges: &[(u64, u64)],
511 blocks: &[Vec<NQuin>],
512) -> io::Result<()> {
513 if blocks.len() != block_ranges.len() {
514 return Err(io::Error::new(
515 io::ErrorKind::InvalidInput,
516 "block count mismatch",
517 ));
518 }
519 let mut writer = StreamingQ42VolumeWriter::new(lex)?;
520 for ((seq, quins), declared_range) in blocks.iter().enumerate().zip(block_ranges) {
521 let actual_range = object_range(quins)?;
522 if actual_range != *declared_range {
523 return Err(io::Error::new(
524 io::ErrorKind::InvalidInput,
525 format!(
526 "declared BIDX range {:?} does not match block {seq} object range {:?}",
527 declared_range, actual_range
528 ),
529 ));
530 }
531 writer.push_block(seq as u64, quins)?;
532 }
533 writer.finish(path)
534}
535
536pub fn write_unified_volume_with_entries(
540 path: &Path,
541 lex: &HashMap<u64, LexiconEntry>,
542 block_ranges: &[(u64, u64)],
543 blocks: &[Vec<NQuin>],
544) -> io::Result<()> {
545 if blocks.len() != block_ranges.len() {
546 return Err(io::Error::new(
547 io::ErrorKind::InvalidInput,
548 "block count mismatch",
549 ));
550 }
551 let mut builder = UnifiedVolumeBuilder::with_lex_entries(lex).map_err(lex_error_to_io)?;
552 for ((seq, quins), declared_range) in blocks.iter().enumerate().zip(block_ranges) {
553 let actual_range = object_range(quins)?;
554 if actual_range != *declared_range {
555 return Err(io::Error::new(
556 io::ErrorKind::InvalidInput,
557 format!(
558 "declared BIDX range {:?} does not match block {seq} object range {:?}",
559 declared_range, actual_range
560 ),
561 ));
562 }
563 builder.push_block(seq as u64, quins)?;
564 }
565 builder.finish(path)
566}
567
568pub fn write_volume_root(path: &Path, manifest: &Q42VolumeManifest) -> io::Result<()> {
572 UnifiedVolumeBuilder::with_empty_lex()
573 .with_volume_manifest(manifest)?
574 .finish(path)
575}
576
577pub fn write_volume_root_for_commons(path: &Path, manifest: &Q42VolumeManifest) -> io::Result<()> {
580 UnifiedVolumeBuilder::with_empty_lex()
581 .with_permissive_commons()
582 .with_volume_manifest(manifest)?
583 .finish(path)
584}
585
586pub fn write_volume_root_with_lex(
590 path: &Path,
591 lex: &HashMap<u64, String>,
592 manifest: &Q42VolumeManifest,
593) -> io::Result<()> {
594 UnifiedVolumeBuilder::with_lex_map(lex)
595 .map_err(lex_error_to_io)?
596 .with_volume_manifest(manifest)?
597 .finish(path)
598}
599
600fn lex_error_to_io(error: LexError) -> io::Error {
601 io::Error::new(
602 io::ErrorKind::InvalidInput,
603 format!("invalid Q42LEX: {error:?}"),
604 )
605}
606
607fn object_range(quins: &[NQuin]) -> io::Result<(u64, u64)> {
613 let Some(first) = quins.first() else {
614 return Err(io::Error::new(
615 io::ErrorKind::InvalidInput,
616 "a Q42 SuperBlock must contain at least one Quin",
617 ));
618 };
619 if quins.len() > QUINS_PER_BLOCK {
620 return Err(io::Error::new(
621 io::ErrorKind::InvalidInput,
622 format!(
623 "Q42 SuperBlock contains {} Quins, exceeding capacity {QUINS_PER_BLOCK}",
624 quins.len()
625 ),
626 ));
627 }
628
629 let mut previous = first.object;
630 for quin in &quins[1..] {
631 if quin.object < previous {
632 return Err(io::Error::new(
633 io::ErrorKind::InvalidInput,
634 "Q42 SuperBlock Quins must be sorted by object hash",
635 ));
636 }
637 previous = quin.object;
638 }
639 Ok((first.object, previous))
640}
641
642pub struct UnifiedVolumeBuilder {
644 lex_bytes: Vec<u8>,
645 volume_manifest: Option<Vec<u8>>,
646 block_ranges: Vec<(u64, u64)>,
647 field_ranges: Vec<(u64, u64, u64, u64, u64, u64)>,
648 dir_entries: Vec<BlockDirectoryEntry>,
649 field_postings: Vec<BlockFieldPostings>,
650 data_blob: Vec<u8>,
651 dag_store: crate::git_bridge::DagStore,
653 author_did: u64,
655 last_dag_hash: [u8; 32],
657 last_object_hash: Option<u64>,
659 publication_commons: bool,
660 publication_sanctuary: bool,
661 sanctuary_quin_count: u64,
662}
663
664impl UnifiedVolumeBuilder {
665 pub fn with_lex_map(lex: &HashMap<u64, String>) -> Result<Self, LexError> {
666 Ok(Self {
667 lex_bytes: encode_lex(lex)?,
668 volume_manifest: None,
669 block_ranges: Vec::new(),
670 field_ranges: Vec::new(),
671 dir_entries: Vec::new(),
672 field_postings: Vec::new(),
673 data_blob: Vec::new(),
674 dag_store: crate::git_bridge::DagStore::new(),
675 author_did: 0,
676 last_dag_hash: [0u8; 32],
677 last_object_hash: None,
678 publication_commons: false,
679 publication_sanctuary: false,
680 sanctuary_quin_count: 0,
681 })
682 }
683
684 pub fn with_lex_entries(lex: &HashMap<u64, LexiconEntry>) -> Result<Self, LexError> {
686 Ok(Self {
687 lex_bytes: encode_lex_with_entries(lex)?,
688 volume_manifest: None,
689 block_ranges: Vec::new(),
690 field_ranges: Vec::new(),
691 dir_entries: Vec::new(),
692 field_postings: Vec::new(),
693 data_blob: Vec::new(),
694 dag_store: crate::git_bridge::DagStore::new(),
695 author_did: 0,
696 last_dag_hash: [0u8; 32],
697 last_object_hash: None,
698 publication_commons: false,
699 publication_sanctuary: false,
700 sanctuary_quin_count: 0,
701 })
702 }
703
704 pub fn with_empty_lex() -> Self {
705 Self::with_lex_map(&HashMap::new()).expect("an empty Q42LEX is valid")
706 }
707
708 pub fn with_volume_manifest(mut self, manifest: &Q42VolumeManifest) -> io::Result<Self> {
711 self.volume_manifest = Some(manifest.encode()?);
712 Ok(self)
713 }
714
715 pub fn with_author_did(mut self, did: u64) -> Self {
717 self.author_did = did;
718 self
719 }
720
721 pub fn with_permissive_commons(mut self) -> Self {
723 self.publication_commons = true;
724 self
725 }
726
727 pub fn with_sanctuary(mut self) -> Self {
729 self.publication_sanctuary = true;
730 self
731 }
732
733 pub fn push_block(&mut self, seq_id: u64, quins: &[NQuin]) -> io::Result<()> {
734 let (min_hash, max_hash) = object_range(quins)?;
735 if let Some(previous) = self.last_object_hash {
736 if min_hash < previous {
737 return Err(io::Error::new(
738 io::ErrorKind::InvalidInput,
739 format!(
740 "block {seq_id} starts at object hash {min_hash:#018X}, before prior block maximum {previous:#018X}"
741 ),
742 ));
743 }
744 }
745 self.block_ranges.push((min_hash, max_hash));
746 let mut subject_min = quins[0].subject;
747 let mut subject_max = quins[0].subject;
748 let mut predicate_min = quins[0].predicate;
749 let mut predicate_max = quins[0].predicate;
750 let mut context_min = quins[0].context;
751 let mut context_max = quins[0].context;
752 for quin in &quins[1..] {
753 subject_min = subject_min.min(quin.subject);
754 subject_max = subject_max.max(quin.subject);
755 predicate_min = predicate_min.min(quin.predicate);
756 predicate_max = predicate_max.max(quin.predicate);
757 context_min = context_min.min(quin.context);
758 context_max = context_max.max(quin.context);
759 }
760 self.sanctuary_quin_count += quins
761 .iter()
762 .filter(|quin| volume::quin_requires_sanctuary(quin))
763 .count() as u64;
764 self.field_ranges.push((
765 subject_min,
766 subject_max,
767 predicate_min,
768 predicate_max,
769 context_min,
770 context_max,
771 ));
772 self.field_postings
773 .push(BlockFieldPostings::from_quins(quins));
774 let raw = encode_superblock(seq_id, quins);
775 let compressed = lz4_flex::compress_prepend_size(&raw);
776 self.dir_entries.push(BlockDirectoryEntry {
777 rel_offset: self.data_blob.len() as u64,
778 comp_len: compressed.len() as u32,
779 uncomp_len: SUPERBLOCK_SIZE as u32,
780 });
781 self.data_blob.extend_from_slice(&compressed);
782
783 let ts = std::time::SystemTime::now()
785 .duration_since(std::time::UNIX_EPOCH)
786 .map(|d| d.as_millis() as u64)
787 .unwrap_or(0);
788 let msg = format!("ingest block {seq_id}");
789 self.last_dag_hash = if self.last_dag_hash == [0u8; 32] {
790 self.dag_store
791 .genesis_node(quins, self.author_did, ts, &msg)
792 } else {
793 self.dag_store
794 .commit_node(self.last_dag_hash, quins, self.author_did, ts, &msg)
795 };
796 self.last_object_hash = Some(max_hash);
797 Ok(())
798 }
799
800 pub fn block_count(&self) -> u64 {
801 self.block_ranges.len() as u64
802 }
803
804 pub fn finish(self, path: &Path) -> io::Result<()> {
805 let bytes = self.finish_to_bytes();
806 let out = OpenOptions::new()
807 .create(true)
808 .write(true)
809 .truncate(true)
810 .open(path)?;
811 let mut w = BufWriter::new(out);
812 w.write_all(&bytes)?;
813 w.flush()?;
814 Ok(())
815 }
816
817 pub fn finish_to_bytes(self) -> Vec<u8> {
824 let bidx_bytes = encode_bidx(&self.block_ranges);
825 let field_range_bytes = encode_field_range_index(&self.field_ranges);
826 let postings_bytes = encode_postings_section(&self.field_postings);
827 let block_count = self.block_ranges.len() as u64;
828 let manifest_bytes = self.volume_manifest.as_deref().unwrap_or(&[]);
829
830 let lex_offset = HEADER_SIZE as u64;
831 let manifest_offset = lex_offset + self.lex_bytes.len() as u64;
832 let bidx_offset = manifest_offset + manifest_bytes.len() as u64;
833 let field_range_offset = bidx_offset + bidx_bytes.len() as u64;
834 let postings_offset = field_range_offset + field_range_bytes.len() as u64;
835 let block_dir_offset = postings_offset + postings_bytes.len() as u64;
836 let block_dir_length = block_count * BlockDirectoryEntry::SIZE as u64;
837 let data_offset = block_dir_offset + block_dir_length;
838
839 let assertion_timestamp = std::time::SystemTime::now()
840 .duration_since(std::time::UNIX_EPOCH)
841 .map(|d| d.as_millis() as u64)
842 .unwrap_or(0);
843
844 let dag_bytes = self.dag_store.serialize();
846 let dag_root_offset = if dag_bytes.is_empty() {
847 0
848 } else {
849 data_offset + self.data_blob.len() as u64
850 };
851 let dag_root_length = dag_bytes.len() as u64;
852
853 let merkle_root = if self.last_dag_hash == [0u8; 32] {
855 [0u8; 32]
856 } else {
857 use sha2::{Digest, Sha256};
859 let mut h = Sha256::new();
860 h.update(self.last_dag_hash);
861 h.finalize().into()
862 };
863
864 let mut reserved = [0u8; 80];
865 let mut flags = FLAG_BLOCKS_LZ4 | FLAG_OBJECT_SORTED;
866 if self.publication_sanctuary || self.sanctuary_quin_count > 0 {
867 flags |= FLAG_SANCTUARY;
868 } else if self.publication_commons {
869 flags |= FLAG_PERMISSIVE_COMMONS;
870 }
871 if !self.field_ranges.is_empty() {
872 flags |= FLAG_FIELD_RANGES;
873 reserved[16..24].copy_from_slice(&field_range_offset.to_le_bytes());
874 reserved[24..32].copy_from_slice(&(field_range_bytes.len() as u64).to_le_bytes());
875 }
876 if !self.field_postings.is_empty() {
877 flags |= FLAG_FIELD_POSTINGS;
878 reserved[32..40].copy_from_slice(&postings_offset.to_le_bytes());
879 reserved[40..48].copy_from_slice(&(postings_bytes.len() as u64).to_le_bytes());
880 }
881 if !manifest_bytes.is_empty() {
882 flags |= FLAG_VOLUME_ROOT;
883 reserved[0..8].copy_from_slice(&manifest_offset.to_le_bytes());
884 reserved[8..16].copy_from_slice(&(manifest_bytes.len() as u64).to_le_bytes());
885 }
886 let header = Q42VolumeHeader {
887 magic: Q42_MAGIC,
888 version: Q42_VERSION_V3,
889 flags,
890 lex_offset,
891 lex_length: self.lex_bytes.len() as u64,
892 bidx_offset,
893 bidx_length: bidx_bytes.len() as u64,
894 block_dir_offset,
895 block_dir_length,
896 data_offset,
897 data_length: self.data_blob.len() as u64,
898 block_count,
899 block_size: SUPERBLOCK_SIZE as u32,
900 quins_per_block: QUINS_PER_BLOCK as u32,
901 temporal_index_offset: 0,
902 temporal_index_length: 0,
903 merkle_root,
904 assertion_timestamp,
905 dag_root_offset,
906 dag_root_length,
907 natural_person_did_offset: 0,
908 software_agent_did_offset: 0,
909 _reserved: reserved,
910 };
911
912 let mut out = Vec::with_capacity(
913 HEADER_SIZE
914 + self.lex_bytes.len()
915 + manifest_bytes.len()
916 + bidx_bytes.len()
917 + field_range_bytes.len()
918 + postings_bytes.len()
919 + block_dir_length as usize
920 + self.data_blob.len()
921 + dag_bytes.len(),
922 );
923 out.extend_from_slice(&header_to_bytes(&header));
924 out.extend_from_slice(&self.lex_bytes);
925 out.extend_from_slice(manifest_bytes);
926 out.extend_from_slice(&bidx_bytes);
927 out.extend_from_slice(&field_range_bytes);
928 out.extend_from_slice(&postings_bytes);
929 for entry in &self.dir_entries {
930 entry.write_to(&mut out).expect("Vec<u8> write cannot fail");
932 }
933 out.extend_from_slice(&self.data_blob);
934 if !dag_bytes.is_empty() {
935 out.extend_from_slice(&dag_bytes);
936 }
937 out
938 }
939}
940
941pub struct Q42Volume {
943 mmap: Mmap,
944 header: Q42VolumeHeader,
945}
946
947impl Q42Volume {
948 pub fn open(path: &Path) -> io::Result<Self> {
949 let file = File::open(path)?;
950 let mmap = unsafe { MmapOptions::new().map(&file)? };
951 if mmap.len() < HEADER_SIZE {
952 return Err(io::Error::new(
953 io::ErrorKind::UnexpectedEof,
954 "file too small for Q42 header",
955 ));
956 }
957 let mut hdr_buf = [0u8; HEADER_SIZE];
958 hdr_buf.copy_from_slice(&mmap[0..HEADER_SIZE]);
959 let header = header_from_bytes(&hdr_buf)?;
960 volume::validate_volume_structure(&header, &mmap)?;
961 Ok(Self { mmap, header })
962 }
963
964 pub fn header(&self) -> &Q42VolumeHeader {
965 &self.header
966 }
967
968 pub fn as_bytes(&self) -> &[u8] {
969 &self.mmap
970 }
971
972 pub fn lex_bytes(&self) -> &[u8] {
973 let start = self.header.lex_offset as usize;
974 let end = start + self.header.lex_length as usize;
975 &self.mmap[start..end]
976 }
977
978 pub fn lex_view(&self) -> Result<Q42LexMmap<'_>, LexError> {
979 Q42LexMmap::from_bytes(self.lex_bytes())
980 }
981
982 pub fn bidx_bytes(&self) -> &[u8] {
983 let start = self.header.bidx_offset as usize;
984 let end = start + self.header.bidx_length as usize;
985 &self.mmap[start..end]
986 }
987
988 pub fn volume_manifest_bytes(&self) -> io::Result<Option<&[u8]>> {
990 let Some((offset, length)) = self.header.volume_manifest_range() else {
991 return Ok(None);
992 };
993 let length = usize::try_from(length).map_err(|_| {
994 io::Error::new(
995 io::ErrorKind::InvalidData,
996 "Q42 manifest length exceeds platform",
997 )
998 })?;
999 if length == 0 || length > MAX_VOLUME_MANIFEST_BYTES {
1000 return Err(io::Error::new(
1001 io::ErrorKind::InvalidData,
1002 "Q42 root has an invalid embedded volume manifest length",
1003 ));
1004 }
1005 let start = usize::try_from(offset).map_err(|_| {
1006 io::Error::new(
1007 io::ErrorKind::InvalidData,
1008 "Q42 manifest offset exceeds platform",
1009 )
1010 })?;
1011 let end = start.checked_add(length).ok_or_else(|| {
1012 io::Error::new(io::ErrorKind::InvalidData, "Q42 manifest range overflows")
1013 })?;
1014 self.mmap.get(start..end).map(Some).ok_or_else(|| {
1015 io::Error::new(
1016 io::ErrorKind::InvalidData,
1017 "Q42 manifest lies outside the file",
1018 )
1019 })
1020 }
1021
1022 pub fn volume_manifest(&self) -> io::Result<Option<Q42VolumeManifest>> {
1025 self.volume_manifest_bytes()?
1026 .map(Q42VolumeManifest::decode)
1027 .transpose()
1028 }
1029
1030 pub fn block_count(&self) -> u64 {
1031 self.header.block_count
1032 }
1033
1034 pub fn verify_all_blocks(&self) -> io::Result<Q42VerificationReceipt> {
1039 if self.volume_manifest()?.is_some() {
1040 return Err(io::Error::new(
1041 io::ErrorKind::InvalidInput,
1042 "verify a Q42 logical root through its manifest child segments",
1043 ));
1044 }
1045 let mut decoded = [0u8; SUPERBLOCK_SIZE];
1046 let mut quins_verified = 0u64;
1047 let mut previous_object = None;
1048 for block_index in 0..self.block_count() as usize {
1049 self.read_superblock_into(block_index, &mut decoded)?;
1050 let live = u64::from_le_bytes(decoded[16..24].try_into().unwrap()) as usize;
1051 if live == 0 || live > QUINS_PER_BLOCK {
1052 return Err(io::Error::new(
1053 io::ErrorKind::InvalidData,
1054 "Q42 SuperBlock has invalid live Quin count",
1055 ));
1056 }
1057 let bidx_offset = 16 + block_index * 16;
1058 let bidx = self.bidx_bytes();
1059 let advertised_min =
1060 u64::from_le_bytes(bidx[bidx_offset..bidx_offset + 8].try_into().unwrap());
1061 let advertised_max =
1062 u64::from_le_bytes(bidx[bidx_offset + 8..bidx_offset + 16].try_into().unwrap());
1063 let mut first = None;
1064 let mut last = 0u64;
1065 for quin_index in 0..live {
1066 let offset = SUPERBLOCK_HEADER + quin_index * QUIN_SIZE;
1067 let quin: crate::NQuin =
1068 bytemuck::pod_read_unaligned(&decoded[offset..offset + QUIN_SIZE]);
1069 if !quin.verify_ecc_parity() {
1070 return Err(io::Error::new(
1071 io::ErrorKind::InvalidData,
1072 format!("Q42 parity mismatch in block {block_index}, Quin {quin_index}"),
1073 ));
1074 }
1075 if previous_object.is_some_and(|previous| quin.object < previous) {
1076 return Err(io::Error::new(
1077 io::ErrorKind::InvalidData,
1078 format!("Q42 object order regresses in block {block_index}"),
1079 ));
1080 }
1081 first.get_or_insert(quin.object);
1082 last = quin.object;
1083 previous_object = Some(quin.object);
1084 quins_verified += 1;
1085 }
1086 if first != Some(advertised_min) || last != advertised_max {
1087 return Err(io::Error::new(
1088 io::ErrorKind::InvalidData,
1089 format!("Q42 BIDX range disagrees with decoded block {block_index}"),
1090 ));
1091 }
1092 }
1093 Ok(Q42VerificationReceipt {
1094 blocks_verified: self.block_count(),
1095 quins_verified,
1096 })
1097 }
1098
1099 pub fn bidx_blocks_for_hash(&self, object_hash: u64) -> Vec<usize> {
1105 bidx_blocks_for_hash(self.bidx_bytes(), object_hash)
1106 }
1107
1108 pub fn bidx_block_range_for_hash(
1110 &self,
1111 object_hash: u64,
1112 ) -> io::Result<Option<BidxBlockRange>> {
1113 volume::bidx_block_range_for_hash(self.bidx_bytes(), object_hash)
1114 }
1115
1116 pub fn bidx_blocks_for_hash_into(
1122 &self,
1123 object_hash: u64,
1124 cursor: usize,
1125 out: &mut [usize],
1126 ) -> io::Result<Option<BidxMatchPage>> {
1127 volume::bidx_blocks_for_hash_into(self.bidx_bytes(), object_hash, cursor, out)
1128 }
1129
1130 pub fn object_hash_bounds(&self) -> Option<(u64, u64)> {
1133 let count = self.block_count() as usize;
1134 if count == 0 {
1135 return None;
1136 }
1137 let bidx = self.bidx_bytes();
1138 let first = u64::from_le_bytes(bidx[16..24].try_into().ok()?);
1139 let last_offset = 16 + (count - 1) * 16;
1140 let last = u64::from_le_bytes(bidx[last_offset + 8..last_offset + 16].try_into().ok()?);
1141 Some((first, last))
1142 }
1143
1144 pub fn block_directory_entry(&self, index: usize) -> io::Result<BlockDirectoryEntry> {
1145 if index >= self.header.block_count as usize {
1146 return Err(io::Error::new(
1147 io::ErrorKind::InvalidInput,
1148 "block index out of range",
1149 ));
1150 }
1151 let start = self.header.block_dir_offset as usize + index * BlockDirectoryEntry::SIZE;
1152 let end = start + BlockDirectoryEntry::SIZE;
1153 let mut buf = [0u8; 16];
1154 buf.copy_from_slice(&self.mmap[start..end]);
1155 Ok(BlockDirectoryEntry::from_bytes(&buf))
1156 }
1157
1158 pub fn read_superblock_into(&self, index: usize, out: &mut [u8]) -> io::Result<usize> {
1160 if out.len() < SUPERBLOCK_SIZE {
1161 return Err(io::Error::new(
1162 io::ErrorKind::InvalidInput,
1163 "output buffer too small",
1164 ));
1165 }
1166 let entry = self.block_directory_entry(index)?;
1167 let data_offset = usize::try_from(self.header.data_offset).map_err(|_| {
1168 io::Error::new(
1169 io::ErrorKind::InvalidData,
1170 "Q42 data offset does not fit this platform",
1171 )
1172 })?;
1173 let rel_offset = usize::try_from(entry.rel_offset).map_err(|_| {
1174 io::Error::new(
1175 io::ErrorKind::InvalidData,
1176 "Q42 block offset does not fit this platform",
1177 )
1178 })?;
1179 let start = data_offset.checked_add(rel_offset).ok_or_else(|| {
1180 io::Error::new(
1181 io::ErrorKind::InvalidData,
1182 "Q42 block offset overflows usize",
1183 )
1184 })?;
1185 let end = start.checked_add(entry.comp_len as usize).ok_or_else(|| {
1186 io::Error::new(
1187 io::ErrorKind::InvalidData,
1188 "Q42 block length overflows usize",
1189 )
1190 })?;
1191 let compressed = &self.mmap[start..end];
1192 if compressed.len() < 4 {
1193 return Err(io::Error::new(
1194 io::ErrorKind::InvalidData,
1195 "Q42 LZ4 block is shorter than its size prefix",
1196 ));
1197 }
1198 let declared = u32::from_le_bytes(compressed[0..4].try_into().unwrap()) as usize;
1199 if declared != entry.uncomp_len as usize || declared != SUPERBLOCK_SIZE {
1200 return Err(io::Error::new(
1201 io::ErrorKind::InvalidData,
1202 "Q42 LZ4 size prefix disagrees with the block directory",
1203 ));
1204 }
1205 let decoded =
1206 lz4_flex::decompress_into(&compressed[4..], &mut out[..declared]).map_err(|e| {
1207 io::Error::new(
1208 io::ErrorKind::InvalidData,
1209 format!("LZ4 decompress block {index}: {e}"),
1210 )
1211 })?;
1212 if decoded != declared {
1213 return Err(io::Error::new(
1214 io::ErrorKind::InvalidData,
1215 "Q42 LZ4 block decoded to an unexpected length",
1216 ));
1217 }
1218 Ok(decoded)
1219 }
1220
1221 pub fn read_all_quins(&self) -> io::Result<Vec<crate::NQuin>> {
1229 if self.volume_manifest()?.is_some() {
1230 return Err(io::Error::new(
1231 io::ErrorKind::InvalidInput,
1232 "Q42 logical root has no local graph blocks; open Q42VolumeSet or use read_q42_quins",
1233 ));
1234 }
1235 let rd = |b: &[u8], o: usize| u64::from_le_bytes(b[o..o + 8].try_into().unwrap());
1236 let mut out = Vec::new();
1237 let mut sb = vec![0u8; SUPERBLOCK_SIZE];
1238 for i in 0..self.block_count() as usize {
1239 self.read_superblock_into(i, &mut sb)?;
1240 let quin_count = rd(&sb, 16) as usize;
1241 for k in 0..quin_count {
1242 let o = SUPERBLOCK_HEADER + k * QUIN_SIZE;
1243 if o + QUIN_SIZE > sb.len() {
1244 break;
1245 }
1246 out.push(crate::NQuin {
1247 subject: rd(&sb, o),
1248 predicate: rd(&sb, o + 8),
1249 object: rd(&sb, o + 16),
1250 context: rd(&sb, o + 24),
1251 metadata: rd(&sb, o + 32),
1252 parity: rd(&sb, o + 40),
1253 });
1254 }
1255 }
1256 Ok(out)
1257 }
1258}
1259
1260pub fn bidx_blocks_for_hash(bidx: &[u8], object_hash: u64) -> Vec<usize> {
1262 match volume::bidx_block_range_for_hash(bidx, object_hash) {
1263 Ok(Some(range)) => (range.start..range.end).collect(),
1264 Ok(None) | Err(_) => Vec::new(),
1265 }
1266}
1267
1268#[cfg(test)]
1269mod tests {
1270 use super::*;
1271 use crate::mini_parser::hash_token;
1272 use tempfile::{NamedTempFile, TempDir};
1273
1274 fn sample_quin(subj: &str, pred: &str, obj: &str) -> (NQuin, HashMap<u64, String>) {
1275 let mut lex = HashMap::new();
1276 let sh = hash_token(subj);
1277 let ph = hash_token(pred);
1278 let oh = hash_token(obj);
1279 lex.insert(sh, subj.to_string());
1280 lex.insert(ph, pred.to_string());
1281 lex.insert(oh, obj.to_string());
1282 let q = NQuin {
1283 subject: sh,
1284 predicate: ph,
1285 object: oh,
1286 context: 0,
1287 metadata: 0,
1288 parity: 0,
1289 };
1290 (q, lex)
1291 }
1292
1293 #[test]
1294 fn unified_volume_roundtrip() {
1295 let (q1, mut lex) = sample_quin("Patient", "fever", "True");
1296 let (q2, lex2) = sample_quin("Patient", "has", "pain");
1297 lex.extend(lex2);
1298
1299 let mut blocks = vec![vec![q1], vec![q2]];
1300 blocks.sort_by_key(|chunk| chunk[0].object);
1301
1302 let tmp = NamedTempFile::new().unwrap();
1303 let ranges: Vec<_> = blocks
1304 .iter()
1305 .map(|chunk| {
1306 let h = chunk[0].object;
1307 (h, h)
1308 })
1309 .collect();
1310 write_unified_volume(tmp.path(), &lex, &ranges, &blocks).unwrap();
1311
1312 let vol = Q42Volume::open(tmp.path()).unwrap();
1313 assert_eq!(vol.block_count(), 2);
1314 assert!(vol.lex_view().unwrap().lookup_hash(q1.object).is_some());
1315
1316 let hits = vol.bidx_blocks_for_hash(q1.object);
1317 assert!(!hits.is_empty(), "bidx miss for object hash {}", q1.object);
1318
1319 let mut block = [0u8; SUPERBLOCK_SIZE];
1320 vol.read_superblock_into(hits[0], &mut block).unwrap();
1321 let active = u64::from_le_bytes(block[16..24].try_into().unwrap());
1322 assert_eq!(active, 1);
1323 }
1324
1325 #[test]
1326 fn exhaustive_verifier_checks_decoded_quins_against_bidx() {
1327 let tmp = NamedTempFile::new().unwrap();
1328 let mut quin = NQuin {
1329 subject: 11,
1330 predicate: 22,
1331 object: 33,
1332 context: 44,
1333 metadata: 55,
1334 parity: 0,
1335 };
1336 quin.parity = NQuin::calculate_parity(
1337 quin.subject,
1338 quin.predicate,
1339 quin.object,
1340 quin.context,
1341 quin.metadata,
1342 );
1343 write_unified_volume(
1344 tmp.path(),
1345 &HashMap::new(),
1346 &[(quin.object, quin.object)],
1347 &[vec![quin]],
1348 )
1349 .unwrap();
1350 let volume = Q42Volume::open(tmp.path()).unwrap();
1351 assert_eq!(
1352 volume.verify_all_blocks().unwrap(),
1353 Q42VerificationReceipt {
1354 blocks_verified: 1,
1355 quins_verified: 1,
1356 }
1357 );
1358 }
1359
1360 #[test]
1361 fn logical_root_verifier_checks_all_manifest_children() {
1362 let dir = tempfile::TempDir::new().unwrap();
1363 let child = dir.path().join("child.q42");
1364 let root = dir.path().join("root.q42");
1365 let mut quin = NQuin {
1366 subject: 101,
1367 predicate: 202,
1368 object: 303,
1369 context: 404,
1370 metadata: 0,
1371 parity: 0,
1372 };
1373 quin.recalculate_parity();
1374 write_unified_volume(
1375 &child,
1376 &HashMap::new(),
1377 &[(quin.object, quin.object)],
1378 &[vec![quin]],
1379 )
1380 .unwrap();
1381 let manifest = Q42VolumeManifest {
1382 generation: 1,
1383 segments: vec![
1384 Q42VolumeManifest::segment_from_file(&child, "child.q42".to_string()).unwrap(),
1385 ],
1386 lexicon_segments: Vec::new(),
1387 };
1388 write_volume_root(&root, &manifest).unwrap();
1389 let set = Q42VolumeSet::open_root(&root).unwrap();
1390 assert_eq!(
1391 set.verify_all(&root).unwrap(),
1392 Q42VerificationReceipt {
1393 blocks_verified: 1,
1394 quins_verified: 1,
1395 }
1396 );
1397 }
1398
1399 #[test]
1400 fn bidx_heavy_hitter_is_complete_and_caller_buffered() {
1401 let (q, lex) = sample_quin("shared", "has", "high-frequency-object");
1402 let blocks = vec![vec![q]; 5];
1403 let ranges = vec![(q.object, q.object); 5];
1404 let tmp = NamedTempFile::new().unwrap();
1405 write_unified_volume(tmp.path(), &lex, &ranges, &blocks).unwrap();
1406
1407 let vol = Q42Volume::open(tmp.path()).unwrap();
1408 assert_eq!(vol.bidx_blocks_for_hash(q.object), vec![0, 1, 2, 3, 4]);
1409
1410 let mut page = [usize::MAX; 2];
1411 let first = vol
1412 .bidx_blocks_for_hash_into(q.object, 0, &mut page)
1413 .unwrap()
1414 .unwrap();
1415 assert_eq!(first.range, BidxBlockRange { start: 0, end: 5 });
1416 assert_eq!(first.returned, 2);
1417 assert_eq!(&page, &[0, 1]);
1418 assert_eq!(first.next_cursor, Some(2));
1419
1420 let second = vol
1421 .bidx_blocks_for_hash_into(q.object, first.next_cursor.unwrap(), &mut page)
1422 .unwrap()
1423 .unwrap();
1424 assert_eq!(&page, &[2, 3]);
1425 assert_eq!(second.next_cursor, Some(4));
1426
1427 let third = vol
1428 .bidx_blocks_for_hash_into(q.object, second.next_cursor.unwrap(), &mut page)
1429 .unwrap()
1430 .unwrap();
1431 assert_eq!(third.returned, 1);
1432 assert_eq!(page[0], 4);
1433 assert_eq!(third.next_cursor, None);
1434 assert!(vol.bidx_blocks_for_hash_into(q.object, 0, &mut []).is_err());
1435 }
1436
1437 #[test]
1438 fn embedded_root_manifest_opens_immutable_child_segments() {
1439 let dir = TempDir::new().unwrap();
1440 let root = dir.path().join("root.q42");
1441 let first_path = dir.path().join("segment-000.q42");
1442 let second_path = dir.path().join("segment-001.q42");
1443 let (first_q, first_lex) = sample_quin("s-1", "p", "o-1");
1444 let (second_q, second_lex) = sample_quin("s-2", "p", "o-2");
1445 let mut inputs = vec![(first_q, first_lex), (second_q, second_lex)];
1446 inputs.sort_unstable_by_key(|(quin, _)| quin.object);
1447 write_unified_volume(
1448 &first_path,
1449 &inputs[0].1,
1450 &[(inputs[0].0.object, inputs[0].0.object)],
1451 &[vec![inputs[0].0]],
1452 )
1453 .unwrap();
1454 write_unified_volume(
1455 &second_path,
1456 &inputs[1].1,
1457 &[(inputs[1].0.object, inputs[1].0.object)],
1458 &[vec![inputs[1].0]],
1459 )
1460 .unwrap();
1461 let manifest = Q42VolumeManifest {
1462 generation: 7,
1463 segments: vec![
1464 Q42VolumeManifest::segment_from_file(&first_path, "segment-000.q42".into())
1465 .unwrap(),
1466 Q42VolumeManifest::segment_from_file(&second_path, "segment-001.q42".into())
1467 .unwrap(),
1468 ],
1469 lexicon_segments: vec![],
1470 };
1471 write_volume_root(&root, &manifest).unwrap();
1472
1473 let root_volume = Q42Volume::open(&root).unwrap();
1474 assert_eq!(root_volume.block_count(), 0);
1475 assert_eq!(
1476 root_volume.volume_manifest().unwrap(),
1477 Some(manifest.clone())
1478 );
1479 let set = Q42VolumeSet::open_root(&root).unwrap();
1480 assert_eq!(set.manifest().generation, 7);
1481 assert_eq!(set.segments().len(), 2);
1482 set.verify_segment_hashes(&root).unwrap();
1483 }
1484
1485 #[test]
1486 fn open_rejects_mismatched_directory_length() {
1487 let (q, lex) = sample_quin("s", "p", "o");
1488 let tmp = NamedTempFile::new().unwrap();
1489 write_unified_volume(tmp.path(), &lex, &[(q.object, q.object)], &[vec![q]]).unwrap();
1490 let mut bytes = std::fs::read(tmp.path()).unwrap();
1491 bytes[48..56].copy_from_slice(&0u64.to_le_bytes());
1492 std::fs::write(tmp.path(), bytes).unwrap();
1493 assert!(Q42Volume::open(tmp.path()).is_err());
1494 }
1495
1496 #[test]
1497 fn decompression_rejects_directory_size_disagreement() {
1498 let (q, lex) = sample_quin("s", "p", "o");
1499 let tmp = NamedTempFile::new().unwrap();
1500 write_unified_volume(tmp.path(), &lex, &[(q.object, q.object)], &[vec![q]]).unwrap();
1501 let mut bytes = std::fs::read(tmp.path()).unwrap();
1502 let data_offset = u64::from_le_bytes(bytes[56..64].try_into().unwrap()) as usize;
1503 bytes[data_offset..data_offset + 4].copy_from_slice(&1u32.to_le_bytes());
1504 std::fs::write(tmp.path(), bytes).unwrap();
1505 let vol = Q42Volume::open(tmp.path()).unwrap();
1506 let mut out = [0u8; SUPERBLOCK_SIZE];
1507 assert!(vol.read_superblock_into(0, &mut out).is_err());
1508 }
1509
1510 #[test]
1515 fn read_all_quins_roundtrip() {
1516 let (q1, mut lex) = sample_quin("Alice", "rdf:type", "values:NaturalPerson");
1517 let (q2, lex2) = sample_quin("Bob", "values:claims", "values:Right");
1518 let (q3, lex3) = sample_quin("AcmeCorp", "rdf:type", "values:CorporatePerson");
1519 lex.extend(lex2);
1520 lex.extend(lex3);
1521
1522 let mut blocks = vec![vec![q1], vec![q2], vec![q3]];
1523 blocks.sort_by_key(|chunk| chunk[0].object);
1524 let ranges: Vec<_> = blocks.iter().map(|c| (c[0].object, c[0].object)).collect();
1525
1526 let tmp = NamedTempFile::new().unwrap();
1527 write_unified_volume(tmp.path(), &lex, &ranges, &blocks).unwrap();
1528
1529 let vol = Q42Volume::open(tmp.path()).unwrap();
1530 let quins = vol.read_all_quins().unwrap();
1531 assert_eq!(
1532 quins.len(),
1533 3,
1534 "read_all_quins must return every stored quin"
1535 );
1536 for q in [q1, q2, q3] {
1537 assert!(
1538 quins.iter().any(|r| r.subject == q.subject
1539 && r.predicate == q.predicate
1540 && r.object == q.object),
1541 "read_all_quins dropped a quin: {q:?}"
1542 );
1543 }
1544 }
1545
1546 #[test]
1550 fn streaming_ingest_embeds_recoverable_lex() {
1551 use crate::external_sort::ExternalSorter;
1552 use crate::lexicon::generate_60bit_token;
1553 use crate::sparql_library::parsers::turtle_doc::parse_turtle_doc_into;
1554
1555 let doc = r#"
1556@prefix dc: <http://purl.org/dc/terms/> .
1557@prefix values: <https://ns.webcivics.net/values/> .
1558@prefix doc: <https://ns.webcivics.net/values/inst#> .
1559doc:article-1 a values:Undertaking ;
1560 dc:title "Article 1" ;
1561 values:originalText "Each Member undertakes to suppress forced labour." .
1562"#;
1563 let tmp_dir = std::env::temp_dir().join(format!("q42_lex_rt_{}", std::process::id()));
1564 let mut sorter = ExternalSorter::new(tmp_dir);
1565 parse_turtle_doc_into(doc.as_bytes(), 0, &mut sorter).unwrap();
1566 let out = NamedTempFile::new().unwrap();
1567 sorter.merge(out.path()).unwrap();
1568
1569 let vol = Q42Volume::open(out.path()).unwrap();
1570 let lex = vol
1571 .lex_view()
1572 .expect("volume must carry an embedded front-of-file Q42LEX section");
1573
1574 let lit = "Each Member undertakes to suppress forced labour.";
1576 assert_eq!(
1577 lex.lookup_hash(generate_60bit_token(lit.as_bytes())),
1578 Some(lit)
1579 );
1580 let undertaking = "https://ns.webcivics.net/values/Undertaking";
1582 assert_eq!(
1583 lex.lookup_hash(generate_60bit_token(undertaking.as_bytes())),
1584 Some(undertaking)
1585 );
1586 }
1587
1588 #[test]
1591 fn finish_to_bytes_matches_a_readable_on_disk_volume() {
1592 let (q1, mut lex) = sample_quin("Heart", "geo:bodySystem", "circulatory");
1593 let (q2, lex2) = sample_quin("Lung", "geo:bodySystem", "respiratory");
1594 lex.extend(lex2);
1595 let mut blocks = vec![vec![q1], vec![q2]];
1596 blocks.sort_by_key(|c| c[0].object);
1597
1598 let mut builder = UnifiedVolumeBuilder::with_lex_map(&lex).unwrap();
1599 for (seq, chunk) in blocks.iter().enumerate() {
1600 builder.push_block(seq as u64, chunk).unwrap();
1601 }
1602 let bytes = builder.finish_to_bytes();
1603
1604 assert!(bytes.starts_with(&Q42_MAGIC), "bytes are a Q42 volume");
1605 let tmp = NamedTempFile::new().unwrap();
1607 std::fs::write(tmp.path(), &bytes).unwrap();
1608 let vol = Q42Volume::open(tmp.path()).unwrap();
1609 assert_eq!(vol.block_count(), 2);
1610 let quins = vol.read_all_quins().unwrap();
1611 assert_eq!(quins.len(), 2);
1612 let lexv = vol.lex_view().unwrap();
1613 let vals: Vec<&str> = quins
1614 .iter()
1615 .filter_map(|q| lexv.lookup_hash(q.object))
1616 .collect();
1617 assert!(vals.contains(&"circulatory") && vals.contains(&"respiratory"));
1618 }
1619
1620 #[test]
1621 fn v2_magic_detected() {
1622 let (q, lex) = sample_quin("a", "b", "c");
1623 let tmp = NamedTempFile::new().unwrap();
1624 write_unified_volume(tmp.path(), &lex, &[(q.object, q.object)], &[vec![q]]).unwrap();
1625 assert!(is_unified_volume(tmp.path()).unwrap());
1626 }
1627}
1628
1629pub struct StreamingVolumeAppender {
1632 file: std::fs::File,
1633 header: Q42VolumeHeader,
1634 block_ranges: Vec<(u64, u64)>,
1635 dir_entries: Vec<BlockDirectoryEntry>,
1636 dag_store: crate::git_bridge::DagStore,
1637 author_did: u64,
1638 last_dag_hash: [u8; 32],
1639 last_object_hash: Option<u64>,
1640}
1641
1642impl StreamingVolumeAppender {
1643 pub fn new(path: &std::path::Path) -> std::io::Result<Self> {
1644 let mut file = std::fs::OpenOptions::new()
1645 .create(true)
1646 .read(true)
1647 .write(true)
1648 .open(path)?;
1649
1650 let header = Q42VolumeHeader {
1651 magic: Q42_MAGIC,
1652 version: Q42_VERSION_V3,
1653 flags: FLAG_BLOCKS_LZ4 | FLAG_OBJECT_SORTED,
1654 lex_offset: HEADER_SIZE as u64,
1655 lex_length: 0,
1656 bidx_offset: HEADER_SIZE as u64,
1657 bidx_length: 0,
1658 block_dir_offset: HEADER_SIZE as u64,
1659 block_dir_length: 0,
1660 data_offset: HEADER_SIZE as u64,
1661 data_length: 0,
1662 block_count: 0,
1663 block_size: SUPERBLOCK_SIZE as u32,
1664 quins_per_block: QUINS_PER_BLOCK as u32,
1665 temporal_index_offset: 0,
1666 temporal_index_length: 0,
1667 merkle_root: [0; 32],
1668 assertion_timestamp: 0,
1669 dag_root_offset: 0,
1670 dag_root_length: 0,
1671 natural_person_did_offset: 0,
1672 software_agent_did_offset: 0,
1673 _reserved: [0; 80],
1674 };
1675
1676 if file.metadata()?.len() != 0 {
1677 return Err(io::Error::new(
1678 io::ErrorKind::Unsupported,
1679 "StreamingVolumeAppender cannot reopen a Q42 volume; publish a new immutable segment",
1680 ));
1681 }
1682 {
1683 use std::io::{Seek, SeekFrom, Write};
1684 file.seek(SeekFrom::Start(0))?;
1685 file.write_all(&header_to_bytes(&header))?;
1686 }
1687
1688 Ok(Self {
1689 file,
1690 header,
1691 block_ranges: Vec::new(),
1692 dir_entries: Vec::new(),
1693 dag_store: crate::git_bridge::DagStore::new(),
1694 author_did: 0,
1695 last_dag_hash: [0u8; 32],
1696 last_object_hash: None,
1697 })
1698 }
1699
1700 pub fn with_author_did(mut self, did: u64) -> Self {
1701 self.author_did = did;
1702 self
1703 }
1704
1705 pub fn append_block(&mut self, seq_id: u64, quins: &[NQuin]) -> std::io::Result<()> {
1706 let (min_hash, max_hash) = object_range(quins)?;
1707 if let Some(previous) = self.last_object_hash {
1708 if min_hash < previous {
1709 return Err(io::Error::new(
1710 io::ErrorKind::InvalidInput,
1711 "Q42 streaming blocks must be globally sorted by object hash",
1712 ));
1713 }
1714 }
1715 self.block_ranges.push((min_hash, max_hash));
1716
1717 let raw = encode_superblock(seq_id, quins);
1718 let compressed = lz4_flex::compress_prepend_size(&raw);
1719
1720 use std::io::{Seek, SeekFrom, Write};
1721 let append_offset = self.header.data_offset + self.header.data_length;
1722 self.file.seek(SeekFrom::Start(append_offset))?;
1723
1724 self.dir_entries.push(BlockDirectoryEntry {
1725 rel_offset: self.header.data_length,
1726 comp_len: compressed.len() as u32,
1727 uncomp_len: SUPERBLOCK_SIZE as u32,
1728 });
1729
1730 self.file.write_all(&compressed)?;
1731 self.header.data_length += compressed.len() as u64;
1732 self.header.block_count += 1;
1733
1734 let ts = std::time::SystemTime::now()
1735 .duration_since(std::time::UNIX_EPOCH)
1736 .map(|d| d.as_millis() as u64)
1737 .unwrap_or(0);
1738 let msg = format!("runtime block {}", seq_id);
1739
1740 self.last_dag_hash = if self.last_dag_hash == [0u8; 32] {
1741 self.dag_store
1742 .genesis_node(quins, self.author_did, ts, &msg)
1743 } else {
1744 self.dag_store
1745 .commit_node(self.last_dag_hash, quins, self.author_did, ts, &msg)
1746 };
1747 self.last_object_hash = Some(max_hash);
1748
1749 let bidx_bytes = encode_bidx(&self.block_ranges);
1751 self.header.bidx_offset = self.header.data_offset + self.header.data_length;
1752 self.header.bidx_length = bidx_bytes.len() as u64;
1753 self.file.write_all(&bidx_bytes)?;
1754
1755 self.header.block_dir_offset = self.header.bidx_offset + self.header.bidx_length;
1756 self.header.block_dir_length = (self.dir_entries.len() * BlockDirectoryEntry::SIZE) as u64;
1757 let mut dir_bytes = Vec::with_capacity(self.header.block_dir_length as usize);
1758 for entry in &self.dir_entries {
1759 dir_bytes.extend_from_slice(&entry.rel_offset.to_le_bytes());
1760 dir_bytes.extend_from_slice(&entry.comp_len.to_le_bytes());
1761 dir_bytes.extend_from_slice(&entry.uncomp_len.to_le_bytes());
1762 }
1763 self.file.write_all(&dir_bytes)?;
1764
1765 let dag_bytes = self.dag_store.serialize();
1767 self.header.dag_root_offset = self.header.block_dir_offset + self.header.block_dir_length;
1768 self.header.dag_root_length = dag_bytes.len() as u64;
1769 self.file.write_all(&dag_bytes)?;
1770
1771 use sha2::{Digest, Sha256};
1772 let mut h = Sha256::new();
1773 h.update(self.last_dag_hash);
1774 self.header.merkle_root = h.finalize().into();
1775 self.header.assertion_timestamp = ts;
1776
1777 self.file.seek(SeekFrom::Start(0))?;
1779 self.file.write_all(&header_to_bytes(&self.header))?;
1780 self.file.sync_all()?;
1781
1782 Ok(())
1783 }
1784}