Skip to main content

writerslogic_warm/core/qgraph/vertex/
file.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3//! On-disk form of a [`VGraph`]: every array in one file at a page-aligned offset, so an index
4//! opens by mapping the file (or reading it into one buffer) without decoding or copying.
5//!
6//! Layout, little-endian: the magic, [`HEADER_WORDS`] `u64` header words, one `(offset, bytes)`
7//! pair per section in [`SECTIONS`] order, then the sections, each starting on a 4096-byte
8//! boundary. Sizes that follow from the header (code widths, strides, block geometry) are
9//! recomputed on open rather than trusted, and every section length, neighbour id and upper-layer
10//! id is checked before the index is returned: a damaged file is an error, never an
11//! out-of-bounds read.
12
13use std::fs::File;
14use std::io::{Read, Seek, SeekFrom, Write};
15use std::ops::Deref;
16use std::path::Path;
17use std::ptr::NonNull;
18use std::sync::Arc;
19
20use rayon::prelude::*;
21
22use super::{
23    bfs_order, code_at, put_varint, quantize, quantize4, zigzag, Graph, GraphView, VertexLayout,
24};
25use super::{
26    block_record_bytes, get_varint, unzigzag, Line, Lines, Metric, Rotation, VGraph, VMetric,
27};
28use super::{BLOCK, BLOCK_SEED, NONE, NONE3, SCREEN_SEED};
29
30/// Element types an index array may hold.
31///
32/// # Safety
33/// Implementors are plain data: every bit pattern is a valid value and there is no padding, so
34/// any aligned, initialized bytes may be viewed as `[Self]` and every `[Self]` as bytes.
35pub(super) unsafe trait Plain: Copy + 'static {}
36// SAFETY: integers and floats of these widths have no invalid bit patterns and no padding.
37unsafe impl Plain for u8 {}
38// SAFETY: as above.
39unsafe impl Plain for i8 {}
40// SAFETY: as above.
41unsafe impl Plain for u32 {}
42// SAFETY: as above.
43unsafe impl Plain for u64 {}
44// SAFETY: as above.
45unsafe impl Plain for f32 {}
46// SAFETY: `Line` is `repr(C)` around `[i8; 128]`: size 128, no padding.
47unsafe impl Plain for Line {}
48
49/// Bytes of a plain array.
50fn as_bytes<T: Plain>(xs: &[T]) -> &[u8] {
51    // SAFETY: `T: Plain` has no padding, so its `len * size_of` bytes are initialized.
52    unsafe { std::slice::from_raw_parts(xs.as_ptr().cast::<u8>(), std::mem::size_of_val(xs)) }
53}
54
55/// The read-only buffer an opened index's arrays point into.
56enum Backing {
57    Map(memmap2::Mmap),
58    /// The file read into cache-line-aligned memory; `.1` is the file length.
59    Heap(Vec<Line>, usize),
60}
61
62impl Backing {
63    fn bytes(&self) -> &[u8] {
64        match self {
65            Self::Map(m) => m,
66            Self::Heap(lines, len) => &as_bytes(lines)[..*len],
67        }
68    }
69}
70
71/// What keeps a [`Slab`]'s elements alive (only ever dropped).
72enum Owner<T> {
73    Vec { _buf: Vec<T> },
74    File { _file: Arc<Backing> },
75}
76
77/// One array of a [`VGraph`]: its own vector (a built index) or a range of the shared buffer
78/// of an opened file. The element pointer is resolved once, so an access costs what a slice
79/// access does whatever holds the bytes.
80pub(super) struct Slab<T: Plain> {
81    ptr: NonNull<T>,
82    len: usize,
83    owner: Owner<T>,
84}
85
86// SAFETY: a `Slab` is a view of `len` initialized, aligned elements at `ptr` that `owner` keeps
87// alive and in place (a vector buffer is never reallocated once wrapped; a backing buffer or
88// mapping is never written). Moving or sharing it moves or shares no more than `&[T]` would.
89unsafe impl<T: Plain + Send + Sync> Send for Slab<T> {}
90// SAFETY: as for `Send`: shared access only reads.
91unsafe impl<T: Plain + Sync> Sync for Slab<T> {}
92
93impl<T: Plain> From<Vec<T>> for Slab<T> {
94    fn from(mut v: Vec<T>) -> Self {
95        let ptr = NonNull::new(v.as_mut_ptr()).expect("a vector pointer is never null");
96        Self {
97            ptr,
98            len: v.len(),
99            owner: Owner::Vec { _buf: v },
100        }
101    }
102}
103
104impl<T: Plain> Deref for Slab<T> {
105    type Target = [T];
106
107    #[inline(always)]
108    fn deref(&self) -> &[T] {
109        // SAFETY: see the `Send` impl.
110        unsafe { std::slice::from_raw_parts(self.ptr.as_ptr(), self.len) }
111    }
112}
113
114impl<T: Plain> Slab<T> {
115    /// The elements of an index being built. Arrays of an opened file are read-only; asking
116    /// for one is a logic error.
117    pub(super) fn as_mut_slice(&mut self) -> &mut [T] {
118        assert!(
119            matches!(self.owner, Owner::Vec { .. }),
120            "the arrays of an opened index file are read-only"
121        );
122        // SAFETY: `ptr` and `len` describe the owned vector's buffer (see `From<Vec<T>>`), and
123        // `&mut self` makes this the only access to it.
124        unsafe { std::slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
125    }
126
127    /// `len` elements of `backing` at byte `offset`, which the caller has checked lie inside
128    /// the buffer at an offset that is a multiple of [`ALIGN`].
129    fn file(backing: &Arc<Backing>, offset: usize, len: usize) -> Self {
130        let ptr = if len == 0 {
131            NonNull::dangling()
132        } else {
133            let bytes = &backing.bytes()[offset..offset + len * std::mem::size_of::<T>()];
134            let p = bytes.as_ptr().cast::<T>().cast_mut();
135            // Buffers start page-aligned (a mapping) or line-aligned (`Vec<Line>`), and section
136            // offsets are multiples of 4096, so every element type is aligned.
137            assert!(p.is_aligned(), "index section is misaligned");
138            NonNull::new(p).expect("a slice pointer is never null")
139        };
140        Self {
141            ptr,
142            len,
143            owner: Owner::File {
144                _file: Arc::clone(backing),
145            },
146        }
147    }
148}
149
150/// Where the arrays of an index opened with [`VGraph::open`] live.
151#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub enum Residency {
153    /// The file is mapped read-only. `open` reads the neighbour lists and id maps, which it
154    /// checks; the codes are read on their first touch. Read pages count as file-backed
155    /// resident memory, which the kernel may reclaim under pressure and read again.
156    Mapped,
157    /// As [`Residency::Mapped`], with every page touched before `open` returns, so no query
158    /// pays a page fault.
159    #[default]
160    Prefaulted,
161    /// The file is read into memory the index owns (anonymous memory).
162    Heap,
163}
164
165const MAGIC: &[u8; 8] = b"HMSVGR01";
166/// Written as a header word; reads back differently on a target of the other byte order.
167const ORDER_MARK: u64 = 0x0102_0304_0506_0708;
168/// Section alignment in the file: a page, so a mapping aligns every section for any element.
169const ALIGN: usize = 4096;
170const HEADER_WORDS: usize = 19;
171/// Section names, in file order (the names appear in errors).
172const SECTIONS: [&str; 15] = [
173    "half_norms",
174    "adj",
175    "adj3",
176    "adjv",
177    "adjv_off",
178    "codes",
179    "scales",
180    "residual",
181    "res_scales",
182    "ids",
183    "screen",
184    "blocks",
185    "mean",
186    "upper_ids",
187    "layers",
188];
189const PREFIX: usize = 8 + 8 * HEADER_WORDS + 16 * SECTIONS.len();
190
191fn bad(message: String) -> std::io::Error {
192    std::io::Error::new(
193        std::io::ErrorKind::InvalidData,
194        format!("index file: {message}"),
195    )
196}
197
198fn le_words(bytes: &[u8]) -> impl Iterator<Item = u64> + '_ {
199    bytes
200        .as_chunks::<8>()
201        .0
202        .iter()
203        .map(|c| u64::from_le_bytes(*c))
204}
205
206/// Upper layers as two `u32` arrays: per layer its vertex count then each list's length, and
207/// all lists concatenated.
208fn encode_layers(layers: &[Vec<Vec<u32>>]) -> Vec<u32> {
209    let mut out = Vec::new();
210    for layer in layers {
211        out.push(layer.len() as u32);
212        out.extend(layer.iter().map(|l| l.len() as u32));
213    }
214    for layer in layers {
215        layer.iter().for_each(|l| out.extend_from_slice(l));
216    }
217    out
218}
219
220fn decode_layers(words: &[u32], count: usize, top: usize) -> std::io::Result<Vec<Vec<Vec<u32>>>> {
221    let short = || bad("layers section is truncated".into());
222    let mut shapes = Vec::with_capacity(count);
223    let mut at = 0;
224    for _ in 0..count {
225        let size = *words.get(at).ok_or_else(short)? as usize;
226        let lens = words.get(at + 1..at + 1 + size).ok_or_else(short)?;
227        shapes.push(lens);
228        at += 1 + size;
229    }
230    let mut layers = Vec::with_capacity(count);
231    let mut prev = top;
232    for lens in shapes {
233        // Layer j + 1 is a prefix of layer j, so the descent's local ids stay valid downwards.
234        if lens.is_empty() || lens.len() > prev {
235            return Err(bad("upper layer sizes do not shrink upwards".into()));
236        }
237        prev = lens.len();
238        let mut layer = Vec::with_capacity(lens.len());
239        for &l in lens {
240            let list = words.get(at..at + l as usize).ok_or_else(short)?;
241            if list.iter().any(|&u| u as usize >= lens.len()) {
242                return Err(bad("upper-layer id out of range".into()));
243            }
244            layer.push(list.to_vec());
245            at += l as usize;
246        }
247        layers.push(layer);
248    }
249    if at != words.len() {
250        return Err(bad("layers section has trailing data".into()));
251    }
252    Ok(layers)
253}
254
255/// The directory a save into `path` writes its temporary file in and syncs: `.` for a bare
256/// file name, whose `parent()` is empty.
257fn parent_dir(path: &Path) -> &Path {
258    path.parent()
259        .filter(|p| !p.as_os_str().is_empty())
260        .unwrap_or_else(|| Path::new("."))
261}
262
263/// `a * b`, or an error naming `what` when it overflows.
264fn mul(a: usize, b: usize, what: &str) -> std::io::Result<usize> {
265    a.checked_mul(b)
266        .ok_or_else(|| bad(format!("{what} overflows")))
267}
268
269impl VGraph {
270    pub(crate) fn release_pages(&self) -> std::io::Result<()> {
271        #[cfg(target_os = "linux")]
272        if let Owner::File { _file } = &self.codes.lines.owner {
273            if let Backing::Map(map) = _file.as_ref() {
274                // SAFETY: generation files are immutable and retained while readers exist;
275                // shared read-only pages reload exactly the same bytes, including during search.
276                unsafe { map.unchecked_advise(memmap2::UncheckedAdvice::DontNeed)? };
277            }
278        }
279        Ok(())
280    }
281
282    pub(crate) fn save_from_graph(
283        g: &Graph,
284        path: &Path,
285        bits: u8,
286        residual_bits: u8,
287        align: bool,
288        reorder: bool,
289        id_bytes: u8,
290    ) -> std::io::Result<()> {
291        save_graph(
292            g.view(),
293            path,
294            bits,
295            residual_bits,
296            align,
297            reorder,
298            id_bytes,
299        )
300    }
301
302    #[cfg(not(target_arch = "wasm32"))]
303    pub(crate) fn save_prepared(
304        g: &super::super::PreparedGraph,
305        path: &Path,
306        bits: u8,
307        residual_bits: u8,
308        align: bool,
309        reorder: bool,
310        id_bytes: u8,
311    ) -> std::io::Result<()> {
312        save_graph(
313            g.view(),
314            path,
315            bits,
316            residual_bits,
317            align,
318            reorder,
319            id_bytes,
320        )
321    }
322
323    /// Dimension of the indexed vectors.
324    pub fn dim(&self) -> usize {
325        self.dim
326    }
327
328    /// The metric the index orders by.
329    pub fn metric(&self) -> VMetric {
330        self.metric
331    }
332
333    /// Writes the index for [`VGraph::open`]: to a temporary file next to `path`, synced, then
334    /// renamed over it, so a reader never sees a partial file. Little-endian targets only (the
335    /// arrays are written as they lie in memory).
336    pub fn save(&self, path: &Path) -> std::io::Result<()> {
337        if cfg!(target_endian = "big") {
338            return Err(std::io::Error::new(
339                std::io::ErrorKind::Unsupported,
340                "index files are little-endian; this target is big-endian",
341            ));
342        }
343        let id_bytes = if !self.adjv_off.is_empty() {
344            0
345        } else if !self.adj3.is_empty() {
346            3
347        } else {
348            4
349        };
350        let header: [u64; HEADER_WORDS] = [
351            ORDER_MARK,
352            self.n as u64,
353            self.dim as u64,
354            self.degree as u64,
355            u64::from(self.bits),
356            u64::from(self.res_bits),
357            self.stride as u64,
358            id_bytes,
359            u64::from(self.entry),
360            self.metric().code(),
361            self.screen_words as u64,
362            u64::from(self.screen_sigma.to_bits()),
363            u64::from(self.block_bits),
364            self.block_dim as u64,
365            self.block_m as u64,
366            self.blocks_per_vertex as u64,
367            u64::from(self.block_kappa.to_bits()),
368            u64::from(!self.ids.is_empty()),
369            self.layers.len() as u64,
370        ];
371        let layers = encode_layers(&self.layers);
372        let sections: [&[u8]; SECTIONS.len()] = [
373            as_bytes(&self.half_norms),
374            as_bytes(&self.adj),
375            as_bytes(&self.adj3),
376            as_bytes(&self.adjv),
377            as_bytes(&self.adjv_off),
378            as_bytes(&self.codes.lines),
379            as_bytes(&self.scales),
380            as_bytes(&self.residual),
381            as_bytes(&self.res_scales),
382            as_bytes(&self.ids),
383            as_bytes(&self.screen),
384            as_bytes(&self.blocks.lines),
385            as_bytes(&self.mean),
386            as_bytes(&self.upper_ids),
387            as_bytes(&layers),
388        ];
389        let mut table = Vec::with_capacity(sections.len());
390        let mut at = PREFIX;
391        for (index, s) in sections.iter().enumerate() {
392            let len = if !self.adj_off.is_empty() && index == if id_bytes == 3 { 2 } else { 1 } {
393                self.n * self.degree * id_bytes as usize
394            } else {
395                s.len()
396            };
397            at = at.next_multiple_of(ALIGN);
398            table.push((at as u64, len as u64));
399            at += len;
400        }
401        if path.file_name().is_none() {
402            return Err(bad(format!("{} names no file", path.display())));
403        }
404        let parent = parent_dir(path);
405        let mut temp = tempfile::NamedTempFile::new_in(parent)?;
406        {
407            let mut w = std::io::BufWriter::with_capacity(1 << 20, temp.as_file_mut());
408            w.write_all(MAGIC)?;
409            for x in header {
410                w.write_all(&x.to_le_bytes())?;
411            }
412            for (off, len) in &table {
413                w.write_all(&off.to_le_bytes())?;
414                w.write_all(&len.to_le_bytes())?;
415            }
416            let mut pos = PREFIX;
417            let zeros = [0u8; ALIGN];
418            for (index, (s, (off, len))) in sections.iter().zip(&table).enumerate() {
419                w.write_all(&zeros[..*off as usize - pos])?;
420                if !self.adj_off.is_empty() && index == if id_bytes == 3 { 2 } else { 1 } {
421                    let width = id_bytes as usize;
422                    let mut row = vec![0xff; self.degree * width];
423                    for v in 0..self.n {
424                        row.fill(0xff);
425                        let mut at = 0;
426                        self.for_each_neighbour(v, |u| {
427                            row[at..at + width].copy_from_slice(&u.to_le_bytes()[..width]);
428                            at += width;
429                        });
430                        w.write_all(&row)?;
431                    }
432                } else {
433                    w.write_all(s)?;
434                }
435                pos = *off as usize + *len as usize;
436            }
437            w.flush()?;
438        }
439        temp.as_file().sync_all()?;
440        crate::core::durable_file::sync_mapped_file(temp.as_file())?;
441        temp.persist(path).map_err(|e| e.error)?;
442        crate::core::durable_file::sync_directory(parent).map_err(std::io::Error::other)
443    }
444
445    /// Opens an index written by [`VGraph::save`] with its arrays where `residency` puts them.
446    /// The search reads the arrays in place, so its results are those of the saved index.
447    ///
448    /// A mapped index (the default, prefaulted) must not be modified or truncated while it is
449    /// open: the mapping would change under the index, and a truncated page faults with
450    /// `SIGBUS`. Replace the file by renaming a new one over it, as [`VGraph::save`] does.
451    pub fn open(path: &Path, residency: Residency) -> std::io::Result<Self> {
452        if cfg!(target_endian = "big") {
453            return Err(std::io::Error::new(
454                std::io::ErrorKind::Unsupported,
455                "index files are little-endian; this target is big-endian",
456            ));
457        }
458        let mut file = File::open(path)?;
459        let len = usize::try_from(file.metadata()?.len())
460            .map_err(|_| bad("larger than the address space".into()))?;
461        if len < PREFIX {
462            return Err(bad(format!("{len} bytes is shorter than the header")));
463        }
464        let backing = Arc::new(match residency {
465            Residency::Heap => {
466                let count = len.div_ceil(std::mem::size_of::<Line>());
467                let mut lines = Vec::with_capacity(count);
468                super::huge_capacity(lines.as_ptr(), count);
469                lines.resize(count, Line([0; 128]));
470                // SAFETY: `Line` is plain bytes (see `Plain`) and `lines` is initialized, so its
471                // first `len <= 128 * count` bytes may be written as `u8`.
472                let buf =
473                    unsafe { std::slice::from_raw_parts_mut(lines.as_mut_ptr().cast::<u8>(), len) };
474                file.read_exact(buf)?;
475                Backing::Heap(lines, len)
476            }
477            // SAFETY: the mapping is read-only and lives in the `Backing`; the caller keeps the
478            // file unmodified while the index is open (see above).
479            Residency::Mapped | Residency::Prefaulted => {
480                Backing::Map(unsafe { memmap2::Mmap::map(&file)? })
481            }
482        });
483        let out = Self::from_backing(&backing)?;
484        if residency == Residency::Prefaulted {
485            let bytes = backing.bytes();
486            let mut sum = 0u8;
487            for at in (0..bytes.len()).step_by(ALIGN) {
488                // SAFETY: `at` is inside `bytes`; a volatile read keeps the touch from being
489                // optimized away.
490                sum ^= unsafe { std::ptr::read_volatile(bytes.as_ptr().add(at)) };
491            }
492            std::hint::black_box(sum);
493        }
494        Ok(out)
495    }
496
497    fn from_backing(backing: &Arc<Backing>) -> std::io::Result<Self> {
498        let bytes = backing.bytes();
499        if bytes.len() < PREFIX {
500            return Err(bad(format!(
501                "{} bytes is shorter than the header",
502                bytes.len()
503            )));
504        }
505        if &bytes[..8] != MAGIC {
506            return Err(bad(
507                "bad magic (not an index file, or another format)".into()
508            ));
509        }
510        let h: Vec<u64> = le_words(&bytes[8..8 + 8 * HEADER_WORDS]).collect();
511        if h[0] != ORDER_MARK {
512            return Err(bad("byte-order mark mismatch".into()));
513        }
514        let small = |i: usize, max: u64, what: &str| -> std::io::Result<usize> {
515            if h[i] > max {
516                Err(bad(format!("{what} {} exceeds {max}", h[i])))
517            } else {
518                Ok(h[i] as usize)
519            }
520        };
521        let n = small(1, u64::from(NONE) - 1, "vertex count")?;
522        let dim = small(2, 1 << 20, "dimension")?;
523        let degree = small(3, 1 << 16, "degree")?;
524        let bits = small(4, 8, "code bits")? as u8;
525        let res_bits = small(5, 8, "residual bits")? as u8;
526        let stride = small(6, 1 << 24, "row stride")?;
527        let id_bytes = small(7, 4, "id width")?;
528        let entry = small(8, u64::from(NONE), "entry")?;
529        let metric = match small(9, 2, "metric")? {
530            0 => VMetric::Angular,
531            1 => VMetric::Euclidean,
532            2 => VMetric::InnerProduct,
533            _ => unreachable!(),
534        };
535        let euclidean = metric == VMetric::Euclidean;
536        let screen_words = small(10, 1 << 20, "screen words")?;
537        let screen_sigma = f32::from_bits(h[11] as u32);
538        let block_bits = small(12, 4, "block bits")? as u8;
539        let block_dim = small(13, 1 << 22, "block dimension")?;
540        let block_m = small(14, 1 << 22, "block groups")?;
541        let blocks_per_vertex = small(15, 1 << 16, "blocks per vertex")?;
542        let block_kappa = f32::from_bits(h[16] as u32);
543        let reordered = small(17, 1, "reorder flag")? == 1;
544        let layer_count = small(18, 64, "layer count")?;
545
546        if n == 0 || dim == 0 || degree == 0 || !degree.is_multiple_of(2) || entry >= n {
547            return Err(bad(
548                "empty index, zero dimension, odd degree or entry out of range".into(),
549            ));
550        }
551        if !matches!(bits, 4 | 8) || !matches!(res_bits, 0 | 4 | 8) {
552            return Err(bad(format!("code bits {bits} / residual bits {res_bits}")));
553        }
554        if !(id_bytes == 4 || id_bytes == 0 || (id_bytes == 3 && (n as u64) < u64::from(NONE3))) {
555            return Err(bad(format!("id width {id_bytes} for {n} vertices")));
556        }
557        // Widths as `from_graph_with` derives them.
558        let padded = dim.next_multiple_of(if bits == 4 { 32 } else { 16 });
559        let cbytes = if bits == 4 { padded / 2 } else { padded };
560        let aligned = if cbytes <= 64 {
561            cbytes.next_power_of_two()
562        } else {
563            cbytes.next_multiple_of(128)
564        };
565        if stride != cbytes && stride != aligned {
566            return Err(bad(format!("row stride {stride} for {cbytes}-byte codes")));
567        }
568        let res_stride = match res_bits {
569            8 => padded,
570            4 => padded / 2,
571            _ => 0,
572        };
573        let screen_rot = (screen_words > 0).then(|| Rotation::new(dim, 0, SCREEN_SEED));
574        if let Some(rot) = &screen_rot {
575            if metric != VMetric::Angular || screen_words != rot.padded() / 64 {
576                return Err(bad("screen shape does not match the dimension".into()));
577            }
578        }
579        let block_rot = (block_bits > 0).then(|| Rotation::new(dim, 0, BLOCK_SEED));
580        let record = match &block_rot {
581            Some(rot) => {
582                if metric != VMetric::Angular
583                    || !matches!(block_bits, 1 | 2 | 4)
584                    || block_dim != rot.padded()
585                    || block_m != block_dim * usize::from(block_bits) / 4
586                    || blocks_per_vertex != degree.div_ceil(BLOCK)
587                {
588                    return Err(bad("neighbour-block shape does not match the graph".into()));
589                }
590                block_record_bytes(block_m)
591            }
592            None if block_dim == 0 && block_m == 0 && blocks_per_vertex == 0 => 0,
593            None => return Err(bad("block shape without blocks".into())),
594        };
595
596        let table: Vec<(usize, usize)> = le_words(&bytes[8 + 8 * HEADER_WORDS..PREFIX])
597            .collect::<Vec<_>>()
598            .as_chunks::<2>()
599            .0
600            .iter()
601            .map(|&[o, l]| (o as usize, l as usize))
602            .collect();
603        let mut end = PREFIX;
604        for (name, &(off, len)) in SECTIONS.iter().zip(&table) {
605            let fits = off.is_multiple_of(ALIGN)
606                && off >= end
607                && off.checked_add(len).is_some_and(|e| e <= bytes.len());
608            if !fits {
609                return Err(bad(format!(
610                    "section {name} lies outside the file or overlaps"
611                )));
612            }
613            end = off + len;
614        }
615        let row_lines = mul(n, stride, "code rows")?.div_ceil(128);
616        let block_bytes = mul(mul(n, blocks_per_vertex, "blocks")?, record, "blocks")?;
617        let nd = mul(n, degree, "adjacency")?;
618        let expect = |s: usize| -> Option<usize> {
619            Some(match s {
620                0 => 4 * if euclidean { n } else { 0 },
621                1 => 4 * if id_bytes == 4 { nd } else { 0 },
622                2 => 3 * if id_bytes == 3 { nd } else { 0 },
623                3 if id_bytes == 0 => return None,
624                3 => 0,
625                4 => 4 * if id_bytes == 0 { n + 1 } else { 0 },
626                5 => 128 * row_lines,
627                6 => 4 * n,
628                7 => n.checked_mul(res_stride)?,
629                8 => 4 * if res_bits > 0 { n } else { 0 },
630                9 => 4 * if reordered { n } else { 0 },
631                10 => {
632                    8 * if screen_words > 0 {
633                        n.checked_mul(screen_words + 1)?
634                    } else {
635                        0
636                    }
637                }
638                11 => 128 * block_bytes.div_ceil(128),
639                12 => 4 * padded,
640                _ => return None,
641            })
642        };
643        for (s, (name, &(_, len))) in SECTIONS.iter().zip(&table).enumerate() {
644            let element = match s {
645                2 | 3 | 7 => 1,
646                5 | 11 => 128,
647                10 => 8,
648                _ => 4,
649            };
650            let ok = match expect(s) {
651                Some(want) => len == want,
652                None => len.is_multiple_of(element),
653            };
654            if !ok {
655                return Err(bad(format!(
656                    "section {name} has {len} bytes, not the expected size"
657                )));
658            }
659        }
660        let sec = |s: usize| table[s];
661        let slab = |s: usize, size: usize| (sec(s).0, sec(s).1 / size);
662        macro_rules! view {
663            ($s:expr, $t:ty) => {{
664                let (off, count) = slab($s, std::mem::size_of::<$t>());
665                Slab::<$t>::file(backing, off, count)
666            }};
667        }
668        let adj = view!(1, u32);
669        let adj3 = view!(2, u8);
670        let adjv = view!(3, u8);
671        let adjv_off = view!(4, u32);
672        let ids = view!(9, u32);
673        let upper_ids: Vec<u32> = view!(13, u32).to_vec();
674        let layer_words = view!(14, u32);
675
676        // Every id the search follows must name a vertex.
677        let in_range = |u: u32, none: u32| (u as usize) < n || u == none;
678        let ids_ok = match id_bytes {
679            4 => adj.iter().all(|&u| in_range(u, NONE)),
680            3 => adj3
681                .as_chunks::<3>()
682                .0
683                .iter()
684                .all(|b| in_range(u32::from_le_bytes([b[0], b[1], b[2], 0]), NONE3)),
685            _ => delta_lists_ok(&adjv, &adjv_off, n, degree),
686        };
687        if !ids_ok {
688            return Err(bad("neighbour id out of range or malformed list".into()));
689        }
690        if ids.iter().any(|&u| u as usize >= n) || upper_ids.iter().any(|&u| u as usize >= n) {
691            return Err(bad("vertex id out of range".into()));
692        }
693        if upper_ids.len() > n {
694            return Err(bad("more upper-layer vertices than vertices".into()));
695        }
696        let layers = decode_layers(&layer_words, layer_count, upper_ids.len())?;
697        let mean: Vec<f32> = view!(12, f32).to_vec();
698        Ok(Self {
699            n,
700            dim,
701            metric,
702            padded,
703            bits,
704            cbytes,
705            stride,
706            degree,
707            mean,
708            half_norms: view!(0, f32),
709            adj,
710            adj3,
711            adj_off: Vec::new(),
712            adjv,
713            adjv_off,
714            codes: Lines {
715                lines: view!(5, Line),
716                len: n * stride,
717            },
718            scales: view!(6, f32),
719            res_bits,
720            res_stride,
721            residual: view!(7, i8),
722            res_scales: view!(8, f32),
723            entry: entry as u32,
724            upper_ids,
725            layers,
726            ids,
727            screen: view!(10, u64),
728            screen_words,
729            screen_rot,
730            screen_sigma,
731            block_bits,
732            block_dim,
733            block_m,
734            blocks_per_vertex,
735            blocks: Lines {
736                lines: view!(11, Line),
737                len: block_bytes,
738            },
739            block_rot,
740            block_kappa,
741        })
742    }
743}
744
745fn section_write<T: Plain>(
746    w: &mut std::io::BufWriter<&mut File>,
747    table: &[(usize, usize)],
748    section: usize,
749    offset: usize,
750    values: &[T],
751) -> std::io::Result<()> {
752    let bytes = as_bytes(values);
753    let (start, len) = table[section];
754    if offset.checked_add(bytes.len()).is_none_or(|end| end > len) {
755        return Err(bad("section write exceeds computed geometry".into()));
756    }
757    w.seek(SeekFrom::Start((start + offset) as u64))?;
758    w.write_all(bytes)
759}
760
761fn save_graph(
762    g: GraphView<'_>,
763    path: &Path,
764    bits: u8,
765    residual_bits: u8,
766    align: bool,
767    reorder: bool,
768    id_bytes: u8,
769) -> std::io::Result<()> {
770    if cfg!(target_endian = "big") {
771        return Err(std::io::Error::new(
772            std::io::ErrorKind::Unsupported,
773            "index files are little-endian",
774        ));
775    }
776    let VertexLayout {
777        padded,
778        cbytes,
779        stride,
780        res_stride,
781    } = VertexLayout::new(g.n, g.dim, bits, residual_bits, align, id_bytes)
782        .map_err(|e| bad(e.to_string()))?;
783    let (n, dim, r) = (g.n, g.dim, g.degree);
784    let ids = if reorder { bfs_order(g) } else { Vec::new() };
785    let old = |v: usize| if reorder { ids[v] as usize } else { v };
786    let mut new_ids = vec![0u32; if reorder { n } else { 0 }];
787    for (v, &id) in ids.iter().enumerate() {
788        new_ids[id as usize] = v as u32;
789    }
790    let new = |v: u32| if reorder { new_ids[v as usize] } else { v };
791    let list = |v: usize| g.neighbors(old(v)).iter().take(r).map(|&v| new(v));
792    let capacity = || bad("encoded index size overflows".into());
793    let mul = |a: usize, b: usize| {
794        a.checked_mul(b)
795            .filter(|&n| n <= isize::MAX as usize)
796            .ok_or_else(capacity)
797    };
798    let mut sorted = Vec::new();
799    let mut encoded = Vec::new();
800    let mut encode_list = |v: usize, dst: &mut Vec<u8>| {
801        sorted.clear();
802        sorted.extend(list(v));
803        sorted.sort_unstable();
804        let mut prev = v as u32;
805        for (i, &u) in sorted.iter().enumerate() {
806            let delta = if i == 0 {
807                zigzag(i64::from(u) - i64::from(prev))
808            } else {
809                u64::from(u - prev - 1)
810            };
811            put_varint(dst, delta);
812            prev = u;
813        }
814    };
815    let mut adjv_len = 0usize;
816    if id_bytes == 0 {
817        for v in 0..n {
818            encoded.clear();
819            encode_list(v, &mut encoded);
820            adjv_len = adjv_len
821                .checked_add(encoded.len())
822                .filter(|&n| n <= u32::MAX as usize)
823                .ok_or_else(capacity)?;
824        }
825    }
826    let layers_len = g.layers.iter().try_fold(0usize, |size, layer| {
827        let words = layer.iter().try_fold(
828            1usize.checked_add(layer.len()).ok_or_else(capacity)?,
829            |words, list| words.checked_add(list.len()).ok_or_else(capacity),
830        )?;
831        size.checked_add(mul(words, 4)?).ok_or_else(capacity)
832    })?;
833    let codes_len = mul(n, stride)?
834        .checked_next_multiple_of(128)
835        .ok_or_else(capacity)?;
836    let lengths = [
837        if g.metric == Metric::Euclidean {
838            mul(n, 4)?
839        } else {
840            0
841        },
842        if id_bytes == 4 {
843            mul(mul(n, r)?, 4)?
844        } else {
845            0
846        },
847        if id_bytes == 3 {
848            mul(mul(n, r)?, 3)?
849        } else {
850            0
851        },
852        adjv_len,
853        if id_bytes == 0 {
854            mul(n.checked_add(1).ok_or_else(capacity)?, 4)?
855        } else {
856            0
857        },
858        codes_len,
859        mul(n, 4)?,
860        mul(n, res_stride)?,
861        if residual_bits > 0 { mul(n, 4)? } else { 0 },
862        mul(ids.len(), 4)?,
863        0,
864        0,
865        mul(padded, 4)?,
866        mul(g.upper_ids.len(), 4)?,
867        layers_len,
868    ];
869    let mut table = Vec::with_capacity(SECTIONS.len());
870    let mut end = PREFIX;
871    for len in lengths {
872        end = end.checked_next_multiple_of(ALIGN).ok_or_else(capacity)?;
873        table.push((end, len));
874        end = end
875            .checked_add(len)
876            .filter(|&n| n <= isize::MAX as usize)
877            .ok_or_else(capacity)?;
878    }
879    let parent = parent_dir(path);
880    let mut temp = tempfile::NamedTempFile::new_in(parent)?;
881    temp.as_file().set_len(end as u64)?;
882    let mut w = std::io::BufWriter::with_capacity(1 << 20, temp.as_file_mut());
883    w.write_all(MAGIC)?;
884    for word in [
885        ORDER_MARK,
886        n as u64,
887        dim as u64,
888        r as u64,
889        u64::from(bits),
890        u64::from(residual_bits),
891        stride as u64,
892        u64::from(id_bytes),
893        u64::from(new(g.entry)),
894        g.metric.code(),
895        0,
896        0,
897        0,
898        0,
899        0,
900        0,
901        0,
902        u64::from(reorder),
903        g.layers.len() as u64,
904    ] {
905        w.write_all(&word.to_le_bytes())?;
906    }
907    for &(offset, len) in &table {
908        w.write_all(&(offset as u64).to_le_bytes())?;
909        w.write_all(&(len as u64).to_le_bytes())?;
910    }
911    let edge_batch = ((1 << 20) / r.saturating_mul(10).max(1)).max(1);
912    let mut adjv_at = 0usize;
913    for start in (0..n).step_by(edge_batch) {
914        let stop = n.min(start + edge_batch);
915        if id_bytes == 0 {
916            let mut offsets = Vec::with_capacity(stop - start);
917            encoded.clear();
918            for v in start..stop {
919                offsets.push((adjv_at + encoded.len()) as u32);
920                encode_list(v, &mut encoded);
921            }
922            section_write(&mut w, &table, 3, adjv_at, &encoded)?;
923            section_write(&mut w, &table, 4, start * 4, &offsets)?;
924            adjv_at += encoded.len();
925        } else if id_bytes == 3 {
926            encoded.clear();
927            for v in start..stop {
928                let mut count = 0;
929                for u in list(v) {
930                    encoded.extend_from_slice(&u.min(NONE3).to_le_bytes()[..3]);
931                    count += 1;
932                }
933                for _ in count..r {
934                    encoded.extend_from_slice(&NONE3.to_le_bytes()[..3]);
935                }
936            }
937            section_write(&mut w, &table, 2, start * r * 3, &encoded)?;
938        } else {
939            let mut edges = vec![NONE; (stop - start) * r];
940            for (v, row) in (start..stop).zip(edges.chunks_exact_mut(r)) {
941                for (dst, u) in row.iter_mut().zip(list(v)) {
942                    *dst = u;
943                }
944            }
945            section_write(&mut w, &table, 1, start * r * 4, &edges)?;
946        }
947    }
948    if id_bytes == 0 {
949        section_write(&mut w, &table, 4, n * 4, &[adjv_at as u32])?;
950    }
951    let mut sum = vec![0f64; dim];
952    for row in g.vectors.chunks_exact(dim) {
953        sum.iter_mut()
954            .zip(row)
955            .for_each(|(s, &v)| *s += f64::from(v));
956    }
957    let mut mean: Vec<f32> = sum.iter().map(|s| (s / n as f64) as f32).collect();
958    mean.resize(padded, 0.0);
959    g.release_rows()?;
960    let batch = ((1 << 20) / (stride + res_stride + 16)).max(1);
961    for start in (0..n).step_by(batch) {
962        let count = batch.min(n - start);
963        let mut codes = vec![0i8; count * stride];
964        let mut scales = vec![0f32; count];
965        let centred = |v: usize| -> Vec<f32> {
966            g.vectors[old(v) * dim..(old(v) + 1) * dim]
967                .iter()
968                .zip(&mean)
969                .map(|(a, m)| a - m)
970                .collect()
971        };
972        codes
973            .par_chunks_exact_mut(stride)
974            .zip(scales.par_iter_mut())
975            .enumerate()
976            .for_each(|(v, (dst, scale))| {
977                let x = centred(start + v);
978                *scale = if bits == 8 {
979                    quantize(&x, &mut dst[..cbytes])
980                } else {
981                    quantize4(&x, &mut dst[..cbytes])
982                };
983            });
984        section_write(&mut w, &table, 5, start * stride, &codes)?;
985        section_write(&mut w, &table, 6, start * 4, &scales)?;
986        if residual_bits > 0 {
987            let mut residual = vec![0i8; count * res_stride];
988            let mut res_scales = vec![0f32; count];
989            residual
990                .par_chunks_exact_mut(res_stride)
991                .zip(res_scales.par_iter_mut())
992                .enumerate()
993                .for_each(|(v, (dst, scale))| {
994                    let mut x = centred(start + v);
995                    let c = &codes[v * stride..v * stride + cbytes];
996                    x.iter_mut()
997                        .enumerate()
998                        .for_each(|(i, a)| *a -= scales[v] * f32::from(code_at(c, bits, i)));
999                    *scale = if residual_bits == 8 {
1000                        quantize(&x, dst)
1001                    } else {
1002                        quantize4(&x, dst)
1003                    };
1004                });
1005            section_write(&mut w, &table, 7, start * res_stride, &residual)?;
1006            section_write(&mut w, &table, 8, start * 4, &res_scales)?;
1007        }
1008        if g.metric == Metric::Euclidean {
1009            let half: Vec<f32> = (start..start + count)
1010                .map(|v| 0.5 * g.norms[old(v)])
1011                .collect();
1012            section_write(&mut w, &table, 0, start * 4, &half)?;
1013        }
1014        g.release_rows()?;
1015    }
1016    section_write(&mut w, &table, 9, 0, &ids)?;
1017    section_write(&mut w, &table, 12, 0, &mean)?;
1018    let upper: Vec<u32> = g.upper_ids.iter().map(|&v| new(v)).collect();
1019    section_write(&mut w, &table, 13, 0, &upper)?;
1020    w.seek(SeekFrom::Start(table[14].0 as u64))?;
1021    for layer in g.layers {
1022        w.write_all(&(layer.len() as u32).to_le_bytes())?;
1023        for list in layer {
1024            w.write_all(&(list.len() as u32).to_le_bytes())?;
1025        }
1026    }
1027    for layer in g.layers {
1028        for list in layer {
1029            w.write_all(as_bytes(list))?;
1030        }
1031    }
1032    w.flush()?;
1033    drop(w);
1034    temp.as_file().sync_all()?;
1035    crate::core::durable_file::sync_mapped_file(temp.as_file())?;
1036    temp.persist(path).map_err(|e| e.error)?;
1037    crate::core::durable_file::sync_directory(parent).map_err(std::io::Error::other)
1038}
1039
1040/// Delta-coded lists (see `VGraph::for_each_delta_neighbour`): offsets rise from 0 to the end
1041/// of `adjv`, each list decodes inside its span to at most `degree` ids below `n`, with
1042/// varints of at most ten bytes.
1043fn delta_lists_ok(adjv: &[u8], off: &[u32], n: usize, degree: usize) -> bool {
1044    if off.len() != n + 1 || off[0] != 0 || off[n] as usize != adjv.len() {
1045        return false;
1046    }
1047    for v in 0..n {
1048        let (a, b) = (off[v] as usize, off[v + 1] as usize);
1049        if a > b || b > adjv.len() {
1050            return false;
1051        }
1052        let span = &adjv[a..b];
1053        let (mut at, mut prev, mut count) = (0, v as i64, 0);
1054        while at < span.len() {
1055            let rest = &span[at..span.len().min(at + 10)];
1056            if rest.iter().all(|&b| b & 0x80 != 0) {
1057                return false;
1058            }
1059            let (delta, used) = get_varint(rest);
1060            at += used;
1061            let u = if count == 0 {
1062                prev.checked_add(unzigzag(delta))
1063            } else {
1064                i64::try_from(delta)
1065                    .ok()
1066                    .and_then(|d| d.checked_add(1))
1067                    .and_then(|d| prev.checked_add(d))
1068            };
1069            match u {
1070                Some(u) if (0..n as i64).contains(&u) => prev = u,
1071                _ => return false,
1072            }
1073            count += 1;
1074            if count > degree {
1075                return false;
1076            }
1077        }
1078    }
1079    true
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085    use crate::core::qgraph::tests::clustered;
1086    use crate::core::qgraph::{BuildParams, Graph, Metric, VSearchParams};
1087
1088    /// Small indexes covering every array the file holds: both metrics, 4- and 8-bit codes,
1089    /// residuals 0/4/8, ids 4/3/delta, reordering, aligned rows, the screen and blocks.
1090    fn index_variants() -> Vec<(String, VGraph, Vec<f32>)> {
1091        let d = 40;
1092        let mut out = Vec::new();
1093        for metric in [Metric::Angular, Metric::Euclidean] {
1094            let data = clustered(2000, d, 7);
1095            let queries = clustered(40, d, 8);
1096            let params = BuildParams {
1097                metric,
1098                ..BuildParams::default()
1099            };
1100            let g = Graph::build(&data, d, &params).expect("build");
1101            type Config = (u8, u8, bool, bool, u8, bool);
1102            let configs: &[Config] = if metric == Metric::Angular {
1103                &[
1104                    (4, 8, true, true, 3, false),
1105                    (8, 4, false, false, 4, true),
1106                    (4, 0, true, true, 0, false),
1107                ]
1108            } else {
1109                &[(8, 8, true, true, 3, false), (4, 4, false, false, 0, false)]
1110            };
1111            for &(bits, res, align, reorder, id_bytes, screen) in configs {
1112                let v = VGraph::from_graph_with(&g, bits, res, align, reorder, id_bytes, screen)
1113                    .expect("encode");
1114                let name =
1115                    format!("{metric:?} b{bits} r{res} a{align} o{reorder} i{id_bytes} s{screen}");
1116                out.push((name, v, queries.clone()));
1117            }
1118            if metric == Metric::Angular {
1119                let v = VGraph::from_graph_with(&g, 4, 8, true, true, 3, false)
1120                    .and_then(|v| v.with_blocks(&g, 2))
1121                    .expect("encode");
1122                out.push(("angular blocks".into(), v, queries.clone()));
1123            } else {
1124                for (bits, id_bytes) in [(4, 0), (8, 3), (8, 4)] {
1125                    let v = VGraph::from_lifted_ip_graph(&g, bits, 8, true, true, id_bytes)
1126                        .expect("encode direct IP");
1127                    let mut probes: Vec<f32> = queries
1128                        .chunks_exact(d)
1129                        .flat_map(|q| q[..d - 1].iter().copied())
1130                        .collect();
1131                    probes.extend(vec![0.0; d - 1]);
1132                    out.push((format!("direct IP b{bits} i{id_bytes}"), v, probes));
1133                }
1134            }
1135        }
1136        out
1137    }
1138
1139    fn same_results(what: &str, a: &VGraph, b: &VGraph, queries: &[f32]) {
1140        let (mut sa, mut sb) = (a.searcher(), b.searcher());
1141        let (mut oa, mut ob) = (Vec::new(), Vec::new());
1142        for q in queries.chunks_exact(a.dim()) {
1143            for (ef, rerank, reservoir) in [(16, 0, 0), (48, 20, 0), (16, 40, 40)] {
1144                if reservoir > 0 && (a.has_screen() || a.has_blocks()) {
1145                    continue;
1146                }
1147                let p = VSearchParams {
1148                    ef,
1149                    rerank,
1150                    reservoir,
1151                    ..VSearchParams::default()
1152                };
1153                let ea = sa.search(q, 10, p, &mut oa).expect("search");
1154                let eb = sb.search(q, 10, p, &mut ob).expect("search");
1155                assert_eq!((ea, &oa), (eb, &ob), "{what}");
1156            }
1157        }
1158    }
1159
1160    #[test]
1161    fn streamed_and_mapped_builds_preserve_exact_vertex_bytes() {
1162        let dir = tempfile::tempdir().unwrap();
1163        let d = 100;
1164        for metric in [Metric::Angular, Metric::Euclidean] {
1165            for n in [4095, 4096, 4097] {
1166                let data = clustered(n, d, 7);
1167                let params = BuildParams {
1168                    metric,
1169                    degree: 8,
1170                    build_ef: 16,
1171                    ..BuildParams::default()
1172                };
1173                let graph = Graph::build(&data, d, &params).unwrap();
1174                let mut rows = super::super::super::PreparedRows::new(n, d).unwrap();
1175                for chunk in data.chunks(17 * d) {
1176                    rows.append(chunk).unwrap();
1177                }
1178                let mapped = rows.build(&params).unwrap();
1179                for (bits, res, align, reorder, id_bytes) in [
1180                    (8, 8, true, false, 4),
1181                    (4, 8, true, true, 3),
1182                    (8, 4, false, false, 0),
1183                    (4, 0, true, true, 0),
1184                ] {
1185                    let owned = dir.path().join("owned");
1186                    let streamed = dir.path().join("streamed");
1187                    let spilled = dir.path().join("mapped");
1188                    let index =
1189                        VGraph::from_graph_with(&graph, bits, res, align, reorder, id_bytes, false)
1190                            .unwrap();
1191                    index.save(&owned).unwrap();
1192                    VGraph::save_from_graph(&graph, &streamed, bits, res, align, reorder, id_bytes)
1193                        .unwrap();
1194                    VGraph::save_prepared(&mapped, &spilled, bits, res, align, reorder, id_bytes)
1195                        .unwrap();
1196                    let expected = std::fs::read(&owned).unwrap();
1197                    assert_eq!(
1198                        std::fs::read(&streamed).unwrap(),
1199                        expected,
1200                        "{metric:?} {n} {bits}/{res} {id_bytes}"
1201                    );
1202                    assert_eq!(
1203                        std::fs::read(&spilled).unwrap(),
1204                        expected,
1205                        "mapped {metric:?} {n} {bits}/{res} {id_bytes}"
1206                    );
1207                    let reopened = VGraph::open(&spilled, Residency::Mapped).unwrap();
1208                    reopened.release_pages().unwrap();
1209                    same_results("mapped encoding", &index, &reopened, &data[..3 * d]);
1210                }
1211            }
1212        }
1213    }
1214
1215    #[test]
1216    fn prepared_rows_reject_incomplete_excess_and_invalid_matrices() {
1217        use super::super::super::PreparedRows;
1218        let params = BuildParams::default();
1219        assert!(PreparedRows::new(1, 0).is_err());
1220        assert!(PreparedRows::new(usize::MAX, 4).is_err());
1221        for count in [1, 2, 3] {
1222            let mut rows = PreparedRows::new(2, 4).unwrap();
1223            let append = rows.append(&vec![1.; count * 4]);
1224            if count > 2 {
1225                assert!(append.is_err());
1226            } else {
1227                append.unwrap();
1228                assert_eq!(rows.build(&params).is_ok(), count == 2);
1229            }
1230        }
1231        let mut rows = PreparedRows::new(1, 4).unwrap();
1232        assert!(rows.append(&[1.; 3]).is_err());
1233        rows.append(&[f32::NAN; 4]).unwrap();
1234        assert!(rows.build(&params).is_err());
1235        let mut rows = PreparedRows::new(1, 4).unwrap();
1236        rows.append(&[f32::MAX; 4]).unwrap();
1237        assert!(rows
1238            .build(&BuildParams {
1239                metric: Metric::Euclidean,
1240                ..params
1241            })
1242            .is_err());
1243    }
1244
1245    #[test]
1246    fn saved_index_opens_with_identical_results_in_every_residency() {
1247        let dir = tempfile::tempdir().unwrap();
1248        for (name, index, queries) in index_variants() {
1249            let path = dir.path().join("index");
1250            index.save(&path).unwrap();
1251            for residency in [Residency::Mapped, Residency::Prefaulted, Residency::Heap] {
1252                let opened = VGraph::open(&path, residency).unwrap();
1253                assert_eq!(opened.index_bytes(), index.index_bytes(), "{name}");
1254                assert_eq!(opened.metric(), index.metric(), "{name}");
1255                same_results(&format!("{name} {residency:?}"), &index, &opened, &queries);
1256            }
1257        }
1258    }
1259
1260    #[test]
1261    fn damaged_files_are_errors() {
1262        let dir = tempfile::tempdir().unwrap();
1263        let (_, index, _) = index_variants().swap_remove(0);
1264        let path = dir.path().join("a");
1265        index.save(&path).unwrap();
1266        let good = std::fs::read(&path).unwrap();
1267        let open = |bytes: &[u8]| {
1268            let p = dir.path().join("b");
1269            std::fs::write(&p, bytes).unwrap();
1270            VGraph::open(&p, Residency::Heap).map(|_| ())
1271        };
1272        let section = |s: usize| {
1273            let t = 8 + 8 * HEADER_WORDS + 16 * s;
1274            u64::from_le_bytes(good[t..t + 8].try_into().unwrap()) as usize
1275        };
1276        assert!(open(&good).is_ok());
1277        assert!(open(&good[..100]).is_err(), "truncated header");
1278        assert!(open(&good[..section(5) + 10]).is_err(), "truncated codes");
1279        let mut bytes = good.clone();
1280        let at = section(2);
1281        bytes[at..at + 3].copy_from_slice(&[0xfe, 0xff, 0xff]);
1282        let err = open(&bytes).unwrap_err().to_string();
1283        assert!(err.contains("neighbour id"), "{err}");
1284        let mut bytes = good.clone();
1285        bytes[8 + 8 * 6] ^= 1;
1286        assert!(open(&bytes).is_err(), "stride");
1287        let mut bytes = good;
1288        bytes[7] = b'9';
1289        assert!(open(&bytes).is_err(), "magic");
1290    }
1291
1292    #[test]
1293    fn damaged_delta_offsets_are_errors() {
1294        let dir = tempfile::tempdir().unwrap();
1295        let (name, index, _) = index_variants().swap_remove(2);
1296        assert!(name.contains("i0"), "{name}");
1297        let path = dir.path().join("a");
1298        index.save(&path).unwrap();
1299        let mut bytes = std::fs::read(&path).unwrap();
1300        let t = 8 + 8 * HEADER_WORDS + 16 * 4;
1301        let offsets = u64::from_le_bytes(bytes[t..t + 8].try_into().unwrap()) as usize;
1302        // Vertex 0's list ends past the delta bytes; the last offset is still their length.
1303        bytes[offsets + 4..offsets + 8].copy_from_slice(&u32::MAX.to_le_bytes());
1304        let damaged = dir.path().join("b");
1305        std::fs::write(&damaged, &bytes).unwrap();
1306        for residency in [Residency::Mapped, Residency::Heap] {
1307            let err = VGraph::open(&damaged, residency)
1308                .err()
1309                .expect("damaged offsets");
1310            assert!(err.to_string().contains("neighbour id"), "{err}");
1311        }
1312
1313        // A later delta of i64::MAX: the next id overflows instead of naming a vertex.
1314        let mut adjv = vec![0x02];
1315        adjv.extend([0xff; 9]);
1316        adjv.push(0x00);
1317        let end = adjv.len() as u32;
1318        assert!(!delta_lists_ok(&adjv, &[0, end, end], 2, 2));
1319        assert!(delta_lists_ok(&[0x02, 0x00], &[0, 2, 2, 2], 3, 2));
1320    }
1321
1322    #[test]
1323    fn a_backing_shorter_than_the_header_is_an_error() {
1324        let mut line = Line([0; 128]);
1325        for (dst, &src) in line.0.iter_mut().zip(MAGIC) {
1326            *dst = src as i8;
1327        }
1328        let backing = Arc::new(Backing::Heap(vec![line], 100));
1329        assert!(VGraph::from_backing(&backing).is_err());
1330    }
1331
1332    #[test]
1333    fn save_leaves_a_sibling_temp_name_alone() {
1334        let dir = tempfile::tempdir().unwrap();
1335        let (_, index, queries) = index_variants().swap_remove(0);
1336        let path = dir.path().join("index");
1337        let sibling = dir.path().join("index.tmp");
1338        std::fs::write(&sibling, b"another writer's bytes").unwrap();
1339        index.save(&path).unwrap();
1340        assert_eq!(std::fs::read(&sibling).unwrap(), b"another writer's bytes");
1341        let opened = VGraph::open(&path, Residency::Heap).unwrap();
1342        same_results("saved beside a temp name", &index, &opened, &queries);
1343        assert_eq!(parent_dir(Path::new("index")), Path::new("."));
1344        assert_eq!(parent_dir(&path), dir.path());
1345    }
1346}