Skip to main content

snomed_ecl_engine/
store.rs

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    /// Word index over description terms. Absent in indexes built before it.
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub search: Option<SearchManifest>,
70    /// Historical associations keyed from both ends. Absent in older indexes,
71    /// which fall back to scanning the association member tables.
72    #[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    /// Value to sources, built on first use.
140    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    /// Concepts with a row whose value is `value`, ascending, once per row.
166    /// The inverse costs four bytes a row and is built on first call.
167    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/// Concrete values retain their RF2 spelling, including decimal precision.
206/// They are semantic values in the core, not display labels.
207#[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    /// Bit 0: active. Bit 1: fully defined.
254    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    /// Active refset member rows referencing concepts. None means the index was not built.
261    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    /// Returns numeric-sorted SCTIDs through active edges, including an inactive self if requested.
279    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    /// Checks that every stored index stays inside its own arrays.
317    ///
318    /// Opening a store runs only these. They are what later evaluation relies
319    /// on to index safely; the semantic invariants in `validate` were proved
320    /// when the index was written, and the section checksum already shows the
321    /// bytes are the same ones.
322    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    /// Checks the bounds above and then every semantic invariant: ordering,
365    /// that the two hierarchy directions agree, that the graph is acyclic, and
366    /// that active rows reference active concepts.
367    ///
368    /// Import runs this before publishing an index and `verify` runs it on
369    /// demand. Opening a store does not, because re-deriving these properties
370    /// on every process start costs more than it protects: the checksum
371    /// detects the corruption they would otherwise catch.
372    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    /// Opens a section without checksumming it.
615    ///
616    /// Hashing the section here read every byte before a query could run, and
617    /// the decode below then read them all again. `verify` hashes every section
618    /// explicitly instead, so integrity is still checked, just not on the path
619    /// that only wants to answer a question.
620    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            // Large sequential reads reduce host/filesystem round trips at startup.
628            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    /// Reads `n` fixed-width records through one scratch buffer.
667    ///
668    /// Pulling each record out of the reader separately costs a `read_exact`
669    /// call per value, and the core holds tens of millions of them. Filling a
670    /// buffer and decoding from the slice keeps the reads large and the decode
671    /// loop tight, without allocating a second copy of the whole column.
672    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}