1use anyhow::{bail, ensure, Context, Result};
2use serde::{Deserialize, Serialize};
3use sha2::{Digest, Sha256};
4use std::collections::VecDeque;
5use std::fs::File;
6use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
7use std::path::Path;
8mod blocks;
9mod columns;
10mod container;
11mod descriptions;
12mod display;
13mod history;
14mod identifiers;
15mod members;
16mod membership;
17mod search;
18mod term_storage;
19mod varint;
20pub(crate) use container::IndexSource;
21pub use container::{pack, pack_with_options, verify, PackOptions, Verification};
22use container::{Section, SectionReader};
23pub use descriptions::DescriptionRow;
24pub use descriptions::{Description, DescriptionIndex, DescriptionManifest, DescriptionStore};
25pub use display::DisplayStore;
26pub use history::{
27 add_history, Association, HistoryIndex, HistoryManifest, HistoryStore, ASSOCIATIONS,
28};
29pub use identifiers::{Identifier, IdentifierIndex, IdentifierManifest, IdentifierStore};
30pub use members::{
31 format_uuid, is_concept_id, parse_uuid, MemberColumn, MemberManifest, MemberStore, MemberTable,
32 MemberValue, TextColumn, ACTIVE, FIELDS, METADATA, REFERENCES,
33};
34pub use membership::{MembershipIndex, MembershipManifest};
35pub use search::{search_pairs, words, SearchIndex, SearchManifest, SearchStore};
36
37pub const FORMAT: u32 = 1;
38const CORE_MAGIC: &[u8; 8] = b"SNECL001";
39
40#[derive(Debug, Serialize, Deserialize)]
41pub struct Manifest {
42 pub format: u32,
43 pub edition: String,
44 pub archive_sha256: String,
45 pub concept_count: usize,
46 pub active_concept_count: usize,
47 pub hierarchy_edges: usize,
48 pub attributes: usize,
49 pub concrete_attributes: usize,
50 pub concrete_values: usize,
51 pub core_bytes: u64,
52 pub core_sha256: String,
53 pub display_bytes: u64,
54 pub display_sha256: String,
55 pub display_refsets: Vec<u64>,
56 pub displays_selected: usize,
57 pub module_dependencies: serde_json::Value,
58 pub capabilities: Vec<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub membership: Option<MembershipManifest>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub descriptions: Option<DescriptionManifest>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub member_tables: Option<Vec<MemberManifest>>,
65 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub identifiers: Option<IdentifierManifest>,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub search: Option<SearchManifest>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub history: Option<HistoryManifest>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub supplements: Vec<RefsetSupplement>,
76}
77
78#[derive(Debug, Serialize, Deserialize)]
79pub struct RefsetSupplement {
80 pub archive_sha256: String,
81 pub release_date: u32,
82 pub base_core_sha256: String,
83 pub refset_ids: Vec<String>,
84 pub added_concepts: usize,
85 pub module_dependencies: serde_json::Value,
86 #[serde(default)]
87 pub exact_module_versions_verified: bool,
88}
89
90impl Manifest {
91 pub fn read(directory: &Path) -> Result<Self> {
92 IndexSource::open(directory).map(|(manifest, _)| manifest)
93 }
94}
95
96#[derive(Debug, Default)]
97pub struct Adjacency {
98 pub offsets: Vec<u32>,
99 pub values: Vec<u32>,
100}
101
102impl Adjacency {
103 pub fn build(count: usize, mut pairs: Vec<(u32, u32)>) -> Result<Self> {
104 pairs.sort_unstable();
105 pairs.dedup();
106 let mut result = Self {
107 offsets: vec![0; count + 1],
108 values: Vec::with_capacity(pairs.len()),
109 };
110 for (source, target) in pairs {
111 ensure!(
112 (source as usize) < count && (target as usize) < count,
113 "Invalid graph endpoint"
114 );
115 result.offsets[source as usize + 1] += 1;
116 result.values.push(target);
117 }
118 prefix_sum(&mut result.offsets)?;
119 Ok(result)
120 }
121
122 pub fn get(&self, ordinal: u32) -> &[u32] {
123 &self.values
124 [self.offsets[ordinal as usize] as usize..self.offsets[ordinal as usize + 1] as usize]
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
129pub struct Attribute {
130 pub group: u32,
131 pub kind: u32,
132 pub value: u32,
133}
134
135#[derive(Debug, Default)]
136pub struct Attributes {
137 pub offsets: Vec<u32>,
138 pub rows: Vec<Attribute>,
139 inverse: std::sync::OnceLock<(Vec<u32>, Vec<u32>)>,
141}
142
143impl Attributes {
144 pub fn build(count: usize, mut rows: Vec<(u32, Attribute)>) -> Result<Self> {
145 rows.sort_unstable();
146 let mut result = Self {
147 offsets: vec![0; count + 1],
148 rows: Vec::with_capacity(rows.len()),
149 inverse: Default::default(),
150 };
151 for (source, attribute) in rows {
152 ensure!((source as usize) < count, "Invalid attribute source");
153 result.offsets[source as usize + 1] += 1;
154 result.rows.push(attribute);
155 }
156 prefix_sum(&mut result.offsets)?;
157 Ok(result)
158 }
159
160 pub fn get(&self, ordinal: u32) -> &[Attribute] {
161 &self.rows
162 [self.offsets[ordinal as usize] as usize..self.offsets[ordinal as usize + 1] as usize]
163 }
164
165 pub fn sources(&self, value: u32) -> &[u32] {
168 let (offsets, sources) = self.inverse.get_or_init(|| {
169 let n = self.offsets.len().saturating_sub(1);
170 let mut offsets = vec![0u32; n + 1];
171 for row in &self.rows {
172 offsets[row.value as usize + 1] += 1;
173 }
174 for i in 1..offsets.len() {
175 offsets[i] += offsets[i - 1];
176 }
177 let mut next = offsets.clone();
178 let mut sources = vec![0u32; self.rows.len()];
179 for source in 0..n {
180 for row in self.get(source as u32) {
181 let slot = &mut next[row.value as usize];
182 sources[*slot as usize] = source as u32;
183 *slot += 1;
184 }
185 }
186 (offsets, sources)
187 });
188 let value = value as usize;
189 match (offsets.get(value), offsets.get(value + 1)) {
190 (Some(&start), Some(&end)) => &sources[start as usize..end as usize],
191 _ => &[],
192 }
193 }
194}
195
196fn prefix_sum(offsets: &mut [u32]) -> Result<()> {
197 for i in 1..offsets.len() {
198 offsets[i] = offsets[i]
199 .checked_add(offsets[i - 1])
200 .context("Index exceeds u32 capacity")?;
201 }
202 Ok(())
203}
204
205#[derive(Debug, PartialEq, Eq)]
208pub enum ConcreteValue {
209 Number(String),
210 Text(String),
211 Boolean(bool),
212}
213
214impl ConcreteValue {
215 pub fn parse(wire: &str) -> Result<Self> {
216 if let Some(number) = wire.strip_prefix('#') {
217 let number = number
218 .strip_prefix('-')
219 .or_else(|| number.strip_prefix('+'))
220 .unwrap_or(number);
221 let parts: Vec<_> = number.split('.').collect();
222 ensure!(
223 parts.len() <= 2
224 && parts
225 .iter()
226 .all(|p| !p.is_empty() && p.bytes().all(|c| c.is_ascii_digit())),
227 "Invalid concrete decimal"
228 );
229 Ok(Self::Number(wire.to_owned()))
230 } else if wire.len() >= 2 && wire.starts_with('"') && wire.ends_with('"') {
231 Ok(Self::Text(wire.to_owned()))
232 } else if wire == "true" || wire == "false" {
233 Ok(Self::Boolean(wire == "true"))
234 } else {
235 bail!("Unsupported concrete value encoding")
236 }
237 }
238
239 fn wire(&self) -> &str {
240 match self {
241 Self::Number(s) | Self::Text(s) => s,
242 Self::Boolean(true) => "true",
243 Self::Boolean(false) => "false",
244 }
245 }
246}
247
248#[derive(Debug, Default)]
249pub struct NumericStore {
250 pub ids: Vec<u64>,
251 pub modules: Vec<u32>,
252 pub effective_times: Vec<u32>,
253 pub flags: Vec<u8>,
255 pub parents: Adjacency,
256 pub children: Adjacency,
257 pub attributes: Attributes,
258 pub concrete: Attributes,
259 pub concrete_values: Vec<ConcreteValue>,
260 pub membership: Option<MembershipIndex>,
262 pub descriptions: DescriptionStore,
263 pub search: SearchStore,
264 pub history: HistoryStore,
265 pub member_tables: MemberStore,
266 pub identifiers: IdentifierStore,
267 pub config: crate::config::QueryConfig,
268}
269
270impl NumericStore {
271 pub fn ordinal(&self, sctid: u64) -> Option<u32> {
272 self.ids.binary_search(&sctid).ok().map(|i| i as u32)
273 }
274 pub fn is_active(&self, ordinal: u32) -> bool {
275 self.flags[ordinal as usize] & 1 != 0
276 }
277
278 pub fn hierarchy(
280 &self,
281 sctid: u64,
282 ancestors: bool,
283 direct: bool,
284 include_self: bool,
285 ) -> Vec<u64> {
286 let Some(start) = self.ordinal(sctid) else {
287 return Vec::new();
288 };
289 let edges = if ancestors {
290 &self.parents
291 } else {
292 &self.children
293 };
294 let mut visited = vec![false; self.ids.len()];
295 let mut stack = vec![start];
296 visited[start as usize] = true;
297 let mut matches = Vec::new();
298 if include_self {
299 matches.push(sctid);
300 }
301 while let Some(source) = stack.pop() {
302 for &target in edges.get(source) {
303 if !visited[target as usize] && self.is_active(target) {
304 visited[target as usize] = true;
305 matches.push(self.ids[target as usize]);
306 if !direct {
307 stack.push(target);
308 }
309 }
310 }
311 }
312 matches.sort_unstable();
313 matches
314 }
315
316 pub fn validate_bounds(&self) -> Result<()> {
323 let n = self.ids.len();
324 if let Some(membership) = &self.membership {
325 membership.validate(n)?;
326 }
327 ensure!(n > 0 && n < u32::MAX as usize, "Invalid concept count");
328 ensure!(
329 self.modules.len() == n && self.effective_times.len() == n && self.flags.len() == n,
330 "Concept section length mismatch"
331 );
332 ensure!(
333 self.modules.iter().all(|&i| (i as usize) < n),
334 "Missing module concept"
335 );
336 ensure!(self.flags.iter().all(|&f| f <= 3), "Invalid concept flags");
337 for graph in [&self.parents, &self.children] {
338 validate_offsets(&graph.offsets, n, graph.values.len())?;
339 ensure!(
340 graph.values.iter().all(|&i| (i as usize) < n),
341 "Invalid hierarchy endpoint"
342 );
343 }
344 ensure!(
345 self.parents.values.len() == self.children.values.len(),
346 "Hierarchy directions disagree"
347 );
348 for (index, value_count) in [
349 (&self.attributes, n),
350 (&self.concrete, self.concrete_values.len()),
351 ] {
352 validate_offsets(&index.offsets, n, index.rows.len())?;
353 ensure!(
354 index
355 .rows
356 .iter()
357 .all(|r| (r.kind as usize) < n && (r.value as usize) < value_count),
358 "Invalid attribute reference"
359 );
360 }
361 Ok(())
362 }
363
364 pub fn validate(&self) -> Result<()> {
373 self.validate_bounds()?;
374 let n = self.ids.len();
375 ensure!(
376 self.ids.windows(2).all(|w| w[0] < w[1]),
377 "Duplicate or unordered concept IDs"
378 );
379 for graph in [&self.parents, &self.children] {
380 for i in 0..n {
381 ensure!(
382 graph.get(i as u32).windows(2).all(|w| w[0] < w[1]),
383 "Duplicate or unordered hierarchy edge"
384 );
385 }
386 }
387 for i in 0..n {
388 for &parent in self.parents.get(i as u32) {
389 ensure!(
390 self.is_active(i as u32) && self.is_active(parent),
391 "Active hierarchy references inactive concept"
392 );
393 ensure!(
394 self.children.get(parent).binary_search(&(i as u32)).is_ok(),
395 "Hierarchy directions disagree"
396 );
397 }
398 }
399 let mut pending: Vec<_> = (0..n).map(|i| self.parents.get(i as u32).len()).collect();
400 let mut ready: VecDeque<_> = (0..n).filter(|&i| pending[i] == 0).collect();
401 let mut processed = 0;
402 while let Some(parent) = ready.pop_front() {
403 processed += 1;
404 for &child in self.children.get(parent as u32) {
405 pending[child as usize] = pending[child as usize]
406 .checked_sub(1)
407 .context("Invalid hierarchy")?;
408 if pending[child as usize] == 0 {
409 ready.push_back(child as usize);
410 }
411 }
412 }
413 ensure!(processed == n, "Hierarchy contains a cycle");
414 for index in [&self.attributes, &self.concrete] {
415 for i in 0..n {
416 ensure!(
417 index.get(i as u32).windows(2).all(|w| w[0] <= w[1]),
418 "Unordered attribute group"
419 );
420 ensure!(
421 index.get(i as u32).is_empty() || self.is_active(i as u32),
422 "Active attribute references inactive source"
423 );
424 }
425 }
426 Ok(())
427 }
428
429 pub fn write(&self, path: &Path) -> Result<()> {
430 let mut out = BufWriter::new(File::create_new(path)?);
431 out.write_all(CORE_MAGIC)?;
432 put_u64(&mut out, self.ids.len() as u64)?;
433 for &id in &self.ids {
434 put_u64(&mut out, id)?;
435 }
436 put_u32s(&mut out, &self.modules)?;
437 put_u32s(&mut out, &self.effective_times)?;
438 put_u64(&mut out, self.flags.len() as u64)?;
439 out.write_all(&self.flags)?;
440 for graph in [&self.parents, &self.children] {
441 put_u32s(&mut out, &graph.offsets)?;
442 put_u32s(&mut out, &graph.values)?;
443 }
444 for index in [&self.attributes, &self.concrete] {
445 put_u32s(&mut out, &index.offsets)?;
446 put_u64(&mut out, index.rows.len() as u64)?;
447 for row in &index.rows {
448 for value in [row.group, row.kind, row.value] {
449 put_u32(&mut out, value)?;
450 }
451 }
452 }
453 put_u64(&mut out, self.concrete_values.len() as u64)?;
454 for value in &self.concrete_values {
455 put_u64(&mut out, value.wire().len() as u64)?;
456 out.write_all(value.wire().as_bytes())?;
457 }
458 out.flush()?;
459 out.get_ref().sync_all()?;
460 Ok(())
461 }
462
463 pub fn open(directory: &Path) -> Result<Self> {
464 let (manifest, source) = IndexSource::open(directory)?;
465 let mut input = Input::open(&source.section("core.bin")?, CORE_MAGIC)?;
466 let count = input.count(8)?;
467 let ids = input.records(count, 8, |b| {
468 u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
469 })?;
470 let modules = input.u32s()?;
471 let effective_times = input.u32s()?;
472 let flags = input.bytes()?;
473 let mut graph = || -> Result<Adjacency> {
474 Ok(Adjacency {
475 offsets: input.u32s()?,
476 values: input.u32s()?,
477 })
478 };
479 let parents = graph()?;
480 let children = graph()?;
481 let mut attributes = || -> Result<Attributes> {
482 let offsets = input.u32s()?;
483 let count = input.count(12)?;
484 let rows = input.records(count, 12, |b| Attribute {
485 group: u32::from_le_bytes([b[0], b[1], b[2], b[3]]),
486 kind: u32::from_le_bytes([b[4], b[5], b[6], b[7]]),
487 value: u32::from_le_bytes([b[8], b[9], b[10], b[11]]),
488 })?;
489 Ok(Attributes {
490 offsets,
491 rows,
492 inverse: Default::default(),
493 })
494 };
495 let attributes_index = attributes()?;
496 let concrete = attributes()?;
497 let value_count = input.count(8)?;
498 let mut concrete_values = Vec::with_capacity(value_count);
499 for _ in 0..value_count {
500 concrete_values.push(ConcreteValue::parse(&String::from_utf8(input.bytes()?)?)?);
501 }
502 ensure!(input.remaining == 0, "Trailing store bytes");
503 let store = Self {
504 ids,
505 modules,
506 effective_times,
507 flags,
508 parents,
509 children,
510 attributes: attributes_index,
511 concrete,
512 concrete_values,
513 membership: manifest
514 .membership
515 .as_ref()
516 .map(|metadata| MembershipIndex::open(&source, metadata, count))
517 .transpose()?,
518 descriptions: manifest
519 .descriptions
520 .map(|m| DescriptionStore::lazy(&source, m, count))
521 .transpose()?
522 .unwrap_or_default(),
523 search: manifest
524 .search
525 .as_ref()
526 .map(|m| SearchStore::lazy(&source, m.clone(), count))
527 .transpose()?
528 .unwrap_or_default(),
529 history: manifest
530 .history
531 .as_ref()
532 .map(|m| HistoryStore::lazy(&source, m.clone(), count))
533 .transpose()?
534 .unwrap_or_default(),
535 member_tables: manifest
536 .member_tables
537 .map(|m| MemberStore::lazy(&source, m))
538 .transpose()?
539 .unwrap_or_default(),
540 identifiers: manifest
541 .identifiers
542 .map(|m| IdentifierStore::lazy(&source, m))
543 .transpose()?
544 .unwrap_or_default(),
545 config: crate::config::QueryConfig::default(),
546 };
547 store.validate_bounds()?;
548 ensure!(
549 store.ids.len() == manifest.concept_count
550 && store.flags.iter().filter(|&&f| f & 1 != 0).count()
551 == manifest.active_concept_count,
552 "Manifest concept counts differ"
553 );
554 ensure!(
555 store.parents.values.len() == manifest.hierarchy_edges
556 && store.attributes.rows.len() == manifest.attributes
557 && store.concrete.rows.len() == manifest.concrete_attributes
558 && store.concrete_values.len() == manifest.concrete_values,
559 "Manifest relationship counts differ"
560 );
561 Ok(store)
562 }
563}
564
565fn validate_offsets(offsets: &[u32], count: usize, values: usize) -> Result<()> {
566 ensure!(
567 offsets.len() == count + 1
568 && offsets.first() == Some(&0)
569 && offsets.last().copied().map(|n| n as usize) == Some(values),
570 "Invalid index offsets"
571 );
572 ensure!(
573 offsets.windows(2).all(|w| w[0] <= w[1]),
574 "Non-monotonic index offsets"
575 );
576 Ok(())
577}
578
579pub fn sha256(path: &Path) -> Result<String> {
580 let mut file = BufReader::new(File::open(path)?);
581 let mut hash = Sha256::new();
582 let mut buffer = vec![0u8; 1024 * 1024];
583 loop {
584 let n = file.read(&mut buffer)?;
585 if n == 0 {
586 break;
587 }
588 hash.update(&buffer[..n]);
589 }
590 Ok(format!("{:x}", hash.finalize()))
591}
592
593fn put_u64(out: &mut impl Write, value: u64) -> Result<()> {
594 out.write_all(&value.to_le_bytes())?;
595 Ok(())
596}
597fn put_u32(out: &mut impl Write, value: u32) -> Result<()> {
598 out.write_all(&value.to_le_bytes())?;
599 Ok(())
600}
601fn put_u32s(out: &mut impl Write, values: &[u32]) -> Result<()> {
602 put_u64(out, values.len() as u64)?;
603 for &value in values {
604 put_u32(out, value)?;
605 }
606 Ok(())
607}
608
609struct Input {
610 reader: BufReader<SectionReader>,
611 remaining: u64,
612}
613impl Input {
614 fn open(section: &Section, magic: &[u8; 8]) -> Result<Self> {
621 let size = section.length;
622 ensure!(
623 (8..=2 * 1024 * 1024 * 1024).contains(&size),
624 "Unsupported store file size"
625 );
626 let mut result = Self {
627 reader: BufReader::with_capacity(1024 * 1024, section.reader()?),
629 remaining: size,
630 };
631 let mut actual = [0; 8];
632 result.read(&mut actual)?;
633 ensure!(
634 &actual == magic,
635 "Unsupported store header; rebuild the index with this version's import"
636 );
637 Ok(result)
638 }
639 fn read(&mut self, bytes: &mut [u8]) -> Result<()> {
640 self.remaining = self
641 .remaining
642 .checked_sub(bytes.len() as u64)
643 .context("Truncated store section")?;
644 self.reader.read_exact(bytes)?;
645 Ok(())
646 }
647 fn u64(&mut self) -> Result<u64> {
648 let mut b = [0; 8];
649 self.read(&mut b)?;
650 Ok(u64::from_le_bytes(b))
651 }
652 fn u32(&mut self) -> Result<u32> {
653 let mut b = [0; 4];
654 self.read(&mut b)?;
655 Ok(u32::from_le_bytes(b))
656 }
657 fn count(&mut self, width: u64) -> Result<usize> {
658 let n = self.u64()?;
659 ensure!(
660 n.checked_mul(width)
661 .is_some_and(|bytes| bytes <= self.remaining),
662 "Invalid store section length"
663 );
664 Ok(usize::try_from(n)?)
665 }
666 fn records<T>(
673 &mut self,
674 n: usize,
675 width: usize,
676 decode: impl Fn(&[u8]) -> T,
677 ) -> Result<Vec<T>> {
678 const SCRATCH: usize = 64 * 1024;
679 let mut out = Vec::with_capacity(n);
680 let per_pass = (SCRATCH / width).max(1);
681 let mut scratch = vec![0; per_pass * width];
682 let mut left = n;
683 while left > 0 {
684 let take = left.min(per_pass);
685 let filled = &mut scratch[..take * width];
686 self.read(filled)?;
687 out.extend(filled.chunks_exact(width).map(&decode));
688 left -= take;
689 }
690 Ok(out)
691 }
692 fn u32s(&mut self) -> Result<Vec<u32>> {
693 let n = self.count(4)?;
694 self.records(n, 4, |b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
695 }
696 fn u16s(&mut self) -> Result<Vec<u16>> {
697 let n = self.count(2)?;
698 self.records(n, 2, |b| u16::from_le_bytes([b[0], b[1]]))
699 }
700 fn bytes(&mut self) -> Result<Vec<u8>> {
701 let n = self.count(1)?;
702 let mut result = vec![0; n];
703 self.read(&mut result)?;
704 Ok(result)
705 }
706}