1use super::columns::{Column, RowSets};
2use super::term_storage::TermStorage;
3use super::*;
4use std::sync::OnceLock;
5
6const MAGIC: &[u8; 8] = b"SNDES001";
7
8#[derive(Clone, Debug, Serialize, Deserialize)]
9pub struct DescriptionManifest {
10 pub bytes: u64,
11 pub sha256: String,
12 pub descriptions: usize,
13 pub active_descriptions: usize,
14 pub language_memberships: usize,
15}
16
17#[derive(Debug)]
19pub struct Description {
20 pub id: u64,
21 pub concept: u32,
22 pub module: u32,
23 pub kind: u32,
24 pub effective_time: u32,
25 pub active: bool,
26 pub language: [u8; 2],
27 pub term: String,
28 pub dialects: Vec<(u32, u32)>,
30}
31
32#[derive(Debug, Default)]
33pub struct DescriptionIndex {
34 pub(super) concepts: Vec<u32>,
35 ids: Vec<u64>,
36 modules: Column,
37 kinds: Column,
38 dates: Column,
39 flags: Column,
40 term_offsets: Vec<u32>,
41 terms: TermStorage,
42 dialects: RowSets,
43}
44
45impl DescriptionIndex {
46 pub fn build(count: usize, mut descriptions: Vec<Description>) -> Result<Self> {
47 descriptions.sort_unstable_by_key(|d| (d.concept, d.id));
48 let mut index = Self {
49 concepts: vec![0; count + 1],
50 term_offsets: vec![0],
51 ..Self::default()
52 };
53 let (mut modules, mut kinds, mut dates, mut flags) =
54 (Vec::new(), Vec::new(), Vec::new(), Vec::new());
55 let (mut dialect_offsets, mut dialects, mut terms) = (vec![0], Vec::new(), String::new());
56 let mut ids = std::collections::HashSet::new();
57 for mut d in descriptions {
58 ensure!(ids.insert(d.id), "Duplicate description ID");
59 ensure!((d.concept as usize) < count, "Unknown description concept");
60 index.concepts[d.concept as usize + 1] += 1;
61 index.ids.push(d.id);
62 modules.push(d.module);
63 kinds.push(d.kind);
64 dates.push(d.effective_time);
65 flags.push(u16::from_le_bytes(d.language) as u32 | (u32::from(d.active) << 16));
66 terms.push_str(&d.term);
67 index.term_offsets.push(u32::try_from(terms.len())?);
68 d.dialects.sort_unstable();
69 d.dialects.dedup();
70 for (refset, acceptability) in d.dialects {
71 dialects.extend([refset, acceptability]);
72 }
73 dialect_offsets.push(u32::try_from(dialects.len())?);
74 }
75 index.modules = Column::new(modules);
76 index.kinds = Column::new(kinds);
77 index.dates = Column::new(dates);
78 index.flags = Column::new(flags);
79 index.dialects = RowSets::new(dialect_offsets, dialects, index.len())?;
80 index.terms = TermStorage::Owned(terms);
81 prefix_sum(&mut index.concepts)?;
82 index.validate(count)?;
83 Ok(index)
84 }
85
86 pub fn len(&self) -> usize {
87 self.ids.len()
88 }
89 pub fn is_empty(&self) -> bool {
90 self.ids.is_empty()
91 }
92 pub fn for_concept(&self, concept: u32) -> std::ops::Range<usize> {
93 self.concepts[concept as usize] as usize..self.concepts[concept as usize + 1] as usize
94 }
95 pub fn id(&self, row: usize) -> u64 {
96 self.ids[row]
97 }
98 pub fn module(&self, row: usize) -> u32 {
99 self.modules.get(row)
100 }
101 pub fn kind(&self, row: usize) -> u32 {
102 self.kinds.get(row)
103 }
104 pub fn effective_time(&self, row: usize) -> u32 {
105 self.dates.get(row)
106 }
107 pub fn active(&self, row: usize) -> bool {
108 self.flags.get(row) & (1 << 16) != 0
109 }
110 pub fn language(&self, row: usize) -> [u8; 2] {
111 (self.flags.get(row) as u16).to_le_bytes()
112 }
113 pub fn term(&self, row: usize) -> Result<String> {
114 self.with_term(row, str::to_owned)
115 }
116 pub fn term_bytes(&self, row: usize) -> usize {
117 (self.term_offsets[row + 1] - self.term_offsets[row]) as usize
118 }
119 pub fn with_term<T>(&self, row: usize, visit: impl FnOnce(&str) -> T) -> Result<T> {
121 self.terms.with_range(
122 self.term_offsets[row] as usize,
123 self.term_offsets[row + 1] as usize,
124 visit,
125 )
126 }
127 pub fn dialects(&self, row: usize) -> impl Iterator<Item = (u32, u32)> + '_ {
128 self.dialects
129 .get(row)
130 .chunks_exact(2)
131 .map(|pair| (pair[0], pair[1]))
132 }
133
134 fn validate(&self, count: usize) -> Result<()> {
135 let n = self.len();
136 validate_offsets(&self.concepts, count, n)?;
137 validate_offsets(&self.term_offsets, n, self.terms.len())?;
138 self.dialects.validate(count, n)?;
139 ensure!(
140 [
141 self.modules.len(),
142 self.kinds.len(),
143 self.dates.len(),
144 self.flags.len()
145 ]
146 .iter()
147 .all(|&v| v == n),
148 "Description column length mismatch"
149 );
150 ensure!(
151 self.modules
152 .iter()
153 .chain(self.kinds.iter())
154 .all(|v| (v as usize) < count),
155 "Invalid description metadata ordinal"
156 );
157 self.terms.validate(&self.term_offsets)?;
158 ensure!(
159 self.term_offsets.windows(2).all(|w| w[0] < w[1]),
160 "Empty description term"
161 );
162 let mut ids = self.ids.clone();
163 ids.sort_unstable();
164 ensure!(
165 ids.iter()
166 .all(|v| (100_000..1_000_000_000_000_000_000).contains(v))
167 && ids.windows(2).all(|w| w[0] < w[1]),
168 "Invalid or duplicate description ID"
169 );
170 for row in 0..n {
171 ensure!(
172 self.flags.get(row) >> 17 == 0
173 && self.language(row).iter().all(u8::is_ascii_lowercase),
174 "Invalid description language or flags"
175 );
176 }
177 Ok(())
178 }
179
180 pub fn write(&self, path: &Path) -> Result<DescriptionManifest> {
181 self.validate(self.concepts.len().saturating_sub(1))?;
182 let mut out = BufWriter::new(File::create_new(path)?);
183 out.write_all(MAGIC)?;
184 put_u32s(&mut out, &self.concepts)?;
185 put_u64(&mut out, self.ids.len() as u64)?;
186 for &id in &self.ids {
187 put_u64(&mut out, id)?;
188 }
189 for column in [&self.modules, &self.kinds, &self.dates, &self.flags] {
190 column.write(&mut out)?;
191 }
192 put_u32s(&mut out, &self.term_offsets)?;
193 self.dialects.write(&mut out)?;
194 self.terms.write(&mut out)?;
195 out.flush()?;
196 out.get_ref().sync_all()?;
197 Ok(DescriptionManifest {
198 bytes: path.metadata()?.len(),
199 sha256: sha256(path)?,
200 descriptions: self.len(),
201 active_descriptions: (0..self.len()).filter(|&i| self.active(i)).count(),
202 language_memberships: self.dialects.entries() / 2,
203 })
204 }
205
206 pub(super) fn open(
207 section: &Section,
208 metadata: &DescriptionManifest,
209 count: usize,
210 ) -> Result<Self> {
211 let mut input = Input::open(section, MAGIC)?;
212 let concepts = input.u32s()?;
213 let n = input.count(8)?;
214 let ids = (0..n).map(|_| input.u64()).collect::<Result<_>>()?;
215 let modules = Column::new(input.u32s()?);
216 let kinds = Column::new(input.u32s()?);
217 let dates = Column::new(input.u32s()?);
218 let flags = Column::new(input.u32s()?);
219 let term_offsets = input.u32s()?;
220 let dialects = RowSets::read(&mut input, n)?;
221 let length = input.count(1)?;
222 ensure!(
223 input.remaining == length as u64,
224 "Trailing description bytes"
225 );
226 let start = input.reader.stream_position()?;
227 let index = Self {
228 concepts,
229 ids,
230 modules,
231 kinds,
232 dates,
233 flags,
234 term_offsets,
235 dialects,
236 terms: TermStorage::stored(section.clone(), start, length)?,
237 };
238 index.validate(count)?;
239 ensure!(
240 index.len() == metadata.descriptions
241 && index.dialects.entries() / 2 == metadata.language_memberships
242 && (0..index.len()).filter(|&r| index.active(r)).count()
243 == metadata.active_descriptions,
244 "Description manifest counts differ"
245 );
246 Ok(index)
247 }
248
249 #[cfg(feature = "import")]
250 pub(crate) fn into_descriptions(self, mapping: &[u32]) -> Result<Vec<Description>> {
251 let mut rows = Vec::with_capacity(self.len());
252 for (concept, &new) in mapping.iter().enumerate() {
253 for row in self.for_concept(concept as u32) {
254 rows.push(Description {
255 id: self.id(row),
256 concept: new,
257 module: mapping[self.module(row) as usize],
258 kind: mapping[self.kind(row) as usize],
259 effective_time: self.effective_time(row),
260 active: self.active(row),
261 language: self.language(row),
262 term: self.term(row)?,
263 dialects: self
264 .dialects(row)
265 .map(|(r, a)| (mapping[r as usize], mapping[a as usize]))
266 .collect(),
267 });
268 }
269 }
270 Ok(rows)
271 }
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct DescriptionRow {
277 pub id: u64,
278 pub module: u32,
279 pub kind: u32,
280 pub effective_time: u32,
281 pub active: bool,
282 pub language: [u8; 2],
283 pub term: String,
284 pub dialects: Vec<(u32, u32)>,
286}
287
288#[derive(Debug)]
292struct Layout {
293 count: usize,
294 rows: u64,
295 concepts: u64,
296 ids: u64,
297 columns: [u64; 4],
299 term_offsets: u64,
300 dialect_offsets: u64,
301 dialect_values: u64,
302 dialect_entries: u64,
303 terms: u64,
304 term_bytes: u64,
305}
306
307#[derive(Debug)]
310struct Seeker {
311 positional: Option<super::container::PositionalReader>,
312 reader: std::sync::Mutex<SectionReader>,
313 layout: Layout,
314}
315impl Seeker {
316 fn open(section: &Section, count: usize) -> Result<Self> {
317 let mut seeker = Self {
318 positional: section.positional()?,
319 reader: std::sync::Mutex::new(section.reader()?),
320 layout: Layout {
321 count,
322 rows: 0,
323 concepts: 0,
324 ids: 0,
325 columns: [0; 4],
326 term_offsets: 0,
327 dialect_offsets: 0,
328 dialect_values: 0,
329 dialect_entries: 0,
330 terms: 0,
331 term_bytes: 0,
332 },
333 };
334 let mut magic = [0; 8];
335 seeker.read(0, &mut magic)?;
336 ensure!(&magic == MAGIC, "Invalid description section");
337 let mut at = 8u64;
339 let mut array = |seeker: &Self, width: u64, expected: Option<u64>| -> Result<(u64, u64)> {
340 let length = seeker.u64(at)?;
341 ensure!(
342 expected.is_none_or(|e| e == length),
343 "Description array length differs"
344 );
345 let start = at + 8;
346 at = length
347 .checked_mul(width)
348 .and_then(|bytes| start.checked_add(bytes))
349 .filter(|&end| end <= section.length)
350 .context("Description array exceeds its section")?;
351 Ok((start, length))
352 };
353 let (concepts, _) = array(&seeker, 4, Some(count as u64 + 1))?;
354 let (ids, rows) = array(&seeker, 8, None)?;
355 let mut columns = [0; 4];
356 for column in &mut columns {
357 *column = array(&seeker, 4, Some(rows))?.0;
358 }
359 let (term_offsets, _) = array(&seeker, 4, Some(rows + 1))?;
360 let (dialect_offsets, _) = array(&seeker, 4, Some(rows + 1))?;
361 let (dialect_values, dialect_entries) = array(&seeker, 4, None)?;
362 let (terms, term_bytes) = array(&seeker, 1, None)?;
363 ensure!(at == section.length, "Trailing description bytes");
364 seeker.layout = Layout {
365 count,
366 rows,
367 concepts,
368 ids,
369 columns,
370 term_offsets,
371 dialect_offsets,
372 dialect_values,
373 dialect_entries,
374 terms,
375 term_bytes,
376 };
377 Ok(seeker)
378 }
379 fn read(&self, position: u64, bytes: &mut [u8]) -> Result<()> {
380 if let Some(reader) = &self.positional {
381 reader.read_exact_at(position, bytes)?;
382 } else {
383 let mut reader = self
384 .reader
385 .lock()
386 .map_err(|_| anyhow::anyhow!("Description reader poisoned"))?;
387 reader.seek(SeekFrom::Start(position))?;
388 reader.read_exact(bytes)?;
389 }
390 Ok(())
391 }
392 fn u64(&self, position: u64) -> Result<u64> {
393 let mut bytes = [0; 8];
394 self.read(position, &mut bytes)?;
395 Ok(u64::from_le_bytes(bytes))
396 }
397 fn u32s(&self, start: u64, first: u64, count: u64) -> Result<Vec<u32>> {
399 let mut bytes = vec![0; usize::try_from(count * 4)?];
400 self.read(start + first * 4, &mut bytes)?;
401 Ok(bytes
402 .chunks_exact(4)
403 .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
404 .collect())
405 }
406 fn rows(&self, concept: u32) -> Result<Vec<DescriptionRow>> {
407 let layout = &self.layout;
408 ensure!(
409 (concept as usize) < layout.count,
410 "Description concept out of range"
411 );
412 let bounds = self.u32s(layout.concepts, concept as u64, 2)?;
413 let (first, last) = (bounds[0] as u64, bounds[1] as u64);
414 ensure!(
415 first <= last && last <= layout.rows,
416 "Invalid description offsets"
417 );
418 let k = last - first;
419 if k == 0 {
420 return Ok(Vec::new());
421 }
422 let mut ids = vec![0; usize::try_from(k * 8)?];
423 self.read(layout.ids + first * 8, &mut ids)?;
424 let [modules, kinds, dates, flags] = layout.columns.map(|at| self.u32s(at, first, k));
425 let (modules, kinds, dates, flags) = (modules?, kinds?, dates?, flags?);
426 let terms = self.u32s(layout.term_offsets, first, k + 1)?;
427 let dialects = self.u32s(layout.dialect_offsets, first, k + 1)?;
428 let (term_start, term_end) = (terms[0] as u64, terms[k as usize] as u64);
429 let (dialect_start, dialect_end) = (dialects[0] as u64, dialects[k as usize] as u64);
430 ensure!(
431 terms.windows(2).all(|w| w[0] < w[1])
432 && term_end <= layout.term_bytes
433 && dialects.windows(2).all(|w| w[0] <= w[1])
434 && dialects.iter().all(|v| v % 2 == 0)
435 && dialect_end <= layout.dialect_entries,
436 "Invalid description text or language offsets"
437 );
438 let mut text = vec![0; usize::try_from(term_end - term_start)?];
439 self.read(layout.terms + term_start, &mut text)?;
440 let members = self.u32s(
441 layout.dialect_values,
442 dialect_start,
443 dialect_end - dialect_start,
444 )?;
445 let count = layout.count as u32;
446 let mut rows = Vec::with_capacity(k as usize);
447 for i in 0..k as usize {
448 let id = u64::from_le_bytes(ids[i * 8..i * 8 + 8].try_into().unwrap());
449 let language = (flags[i] as u16).to_le_bytes();
450 ensure!(
451 modules[i] < count
452 && kinds[i] < count
453 && flags[i] >> 17 == 0
454 && language.iter().all(u8::is_ascii_lowercase)
455 && (100_000..1_000_000_000_000_000_000).contains(&id),
456 "Invalid description row"
457 );
458 let term = &text[(terms[i] as u64 - term_start) as usize
459 ..(terms[i + 1] as u64 - term_start) as usize];
460 let pairs = &members[(dialects[i] as u64 - dialect_start) as usize
461 ..(dialects[i + 1] as u64 - dialect_start) as usize];
462 ensure!(
463 pairs.iter().all(|&v| v < count),
464 "Invalid description dialect ordinal"
465 );
466 rows.push(DescriptionRow {
467 id,
468 module: modules[i],
469 kind: kinds[i],
470 effective_time: dates[i],
471 active: flags[i] & (1 << 16) != 0,
472 language,
473 term: std::str::from_utf8(term)
474 .context("Invalid description UTF-8")?
475 .to_owned(),
476 dialects: pairs.chunks_exact(2).map(|p| (p[0], p[1])).collect(),
477 });
478 }
479 Ok(rows)
480 }
481}
482
483#[derive(Debug, Default)]
485pub struct DescriptionStore {
486 source: Option<(Section, DescriptionManifest, usize)>,
487 loaded: OnceLock<std::result::Result<DescriptionIndex, String>>,
488 seeker: OnceLock<std::result::Result<Seeker, String>>,
489}
490impl DescriptionStore {
491 pub fn loaded(index: DescriptionIndex) -> Self {
492 Self {
493 source: None,
494 loaded: OnceLock::from(Ok(index)),
495 seeker: OnceLock::new(),
496 }
497 }
498 pub(super) fn lazy(
499 source: &IndexSource,
500 metadata: DescriptionManifest,
501 count: usize,
502 ) -> Result<Self> {
503 Ok(Self {
504 source: Some((source.section("descriptions.bin")?, metadata, count)),
505 loaded: OnceLock::new(),
506 seeker: OnceLock::new(),
507 })
508 }
509 pub fn is_available(&self) -> bool {
511 self.source.is_some() || self.loaded.get().is_some()
512 }
513 pub fn is_loaded(&self) -> bool {
515 matches!(self.loaded.get(), Some(Ok(_)))
516 }
517 pub fn concept_rows(&self, concept: u32) -> Result<Option<Vec<DescriptionRow>>> {
521 if let Some(Ok(index)) = self.loaded.get() {
522 return Ok(Some(
523 index
524 .for_concept(concept)
525 .map(|row| {
526 Ok(DescriptionRow {
527 id: index.id(row),
528 module: index.module(row),
529 kind: index.kind(row),
530 effective_time: index.effective_time(row),
531 active: index.active(row),
532 language: index.language(row),
533 term: index.term(row)?,
534 dialects: index.dialects(row).collect(),
535 })
536 })
537 .collect::<Result<_>>()?,
538 ));
539 }
540 let Some((section, _, count)) = &self.source else {
541 return Ok(None);
542 };
543 match self
544 .seeker
545 .get_or_init(|| Seeker::open(section, *count).map_err(|e| e.to_string()))
546 {
547 Ok(seeker) => seeker.rows(concept).map(Some),
548 Err(message) => bail!("Description index: {message}"),
549 }
550 }
551 pub fn get(&self) -> Result<Option<&DescriptionIndex>> {
552 if self.source.is_none() && self.loaded.get().is_none() {
553 return Ok(None);
554 }
555 match self.loaded.get_or_init(|| {
556 let (path, metadata, count) = self.source.as_ref().unwrap();
557 DescriptionIndex::open(path, metadata, *count).map_err(|e| e.to_string())
558 }) {
559 Ok(index) => Ok(Some(index)),
560 Err(message) => bail!("Description index: {message}"),
561 }
562 }
563 #[cfg(feature = "import")]
564 pub(crate) fn into_index(mut self) -> Result<Option<DescriptionIndex>> {
565 self.get()?;
566 self.loaded
567 .take()
568 .map(|r| r.map_err(anyhow::Error::msg))
569 .transpose()
570 }
571}