Skip to main content

yo_format/
image.rs

1//! The index image: what a checkpoint points at so an index does not have to be
2//! rebuilt from the records it was built from.
3//!
4//! [`CheckpointEntry::index_image_addr`](crate::superblock::CheckpointEntry)
5//! has been in the superblock since the format was first written down and
6//! nothing has pointed at anything yet. This is the first thing it points at,
7//! and it is the vector index, because the vector index is the one that costs
8//! real money to rebuild: a million vectors is a million quantisations and a few
9//! thousand partitions that arrived at their shape through a long sequence of
10//! splits, merges and sweeps. Replaying the records gives back the vectors. It
11//! does not give back the shape, and rebuilding the shape on open is the outage
12//! the whole update protocol exists to avoid.
13//!
14//! # It is chunks, not a record kind
15//!
16//! `06` fixes the record kinds and there is no kind for an index, which is
17//! deliberate: an index is derived, so a reader that has never heard of it must
18//! be able to walk straight past it. So an image is written as
19//! [`CollectionChunk`](crate::RecordKind::CollectionChunk) records through the
20//! chain in `yo-kv`, exactly the way a demoted collection is, and the only thing
21//! that knows the chunks mean an index is the checkpoint entry that points at
22//! them. A reader that ignores the checkpoint sees a run of chunks nobody claims
23//! and compaction drops them.
24//!
25//! `10` section 2 is where the shape comes from: a partition is a natural chunk.
26//! At the default posting size and 768 dimensions a partition is about 32 KiB,
27//! which is half of one chunk, so a partition is one chunk and one read almost
28//! always, and a partition that has grown past a chunk is a chain of its own
29//! rather than a special case.
30//!
31//! ```text
32//!   root                        one per partition
33//! +------------------+        +---------------------------+
34//! | header, 100 bytes |        | count | code_bytes | stuck |
35//! +------------------+        +---------------------------+
36//! | centroid chain   |        | ids      count * 8        |
37//! | key chain        |        | tags     count * 8        |
38//! +------------------+        | codes    count * width    |
39//! | partition 0      | -----> | meta     count * 16       |
40//! | partition 1      |        +---------------------------+
41//! | ...              |
42//! +------------------+
43//! ```
44//!
45//! The four arrays inside a partition are separate runs rather than one run of
46//! structures, and that is the same reason the posting itself is laid out that
47//! way in memory: a scan that only wants the tags reads only the tags. A cold
48//! partition can be brought in one array at a time for the same reason.
49//!
50//! # What is not in here
51//!
52//! The vectors. They are records of kind 3 already (`crate::vector`), at
53//! addresses the log resolves, and G8's budget is 96 bytes of index for a 768
54//! dimensional vector with the raw copy in the log. An image that carried them
55//! as well would write every vector twice and spend the whole gate to save a
56//! walk. So loading an image is two halves: the image gives the shape and the
57//! codes, and the log gives the vectors back under the keys the image names.
58//!
59//! No checksum either, and that is not an omission. Every chunk of an image is a
60//! record, every record carries a CRC32C over its own bytes, and the chain's
61//! directory is a record too, so a second checksum inside the image would cover
62//! bytes that are already covered. What a checksum cannot catch is an image that
63//! is intact and stale, and that is what the checkpoint's log addresses are for.
64//!
65//! # The freeze
66//!
67//! This layout is frozen with the rest of the format at the end of M6. After
68//! that the only lever is `min_reader_version` (`07` section 9), so the fields
69//! that exist to be changed later exist now: [`ImageHeader::kind`] so that a
70//! document or graph index can have an image beside this one, `flags` in both
71//! headers so that a section can be added to an image a version one reader then
72//! refuses one image at a time, and `bits` and `metric` as their own bytes
73//! rather than as something a reader has to infer.
74//!
75//! An image is a cache in the end, which is the safety net under all of it: a
76//! reader that does not like an image can throw it away and rebuild from the
77//! records, slowly and correctly.
78
79use crate::{
80    get_f32, get_u8, get_u16, get_u32, get_u64, put_f32, put_u8, put_u16, put_u32, put_u64,
81};
82use yo_common::{Code, Error, Result};
83
84/// The four bytes an image starts with, so that a stray chunk is not read as
85/// one.
86pub const IMAGE_TAG: u32 = u32::from_le_bytes(*b"YOIX");
87
88/// The fixed part at the front of an image root.
89pub const IMAGE_HEADER_LEN: usize = 100;
90
91/// One line of the partition table that follows the root header.
92pub const PARTITION_ENTRY_LEN: usize = 16;
93
94/// The fixed part at the front of one partition's image.
95pub const POSTING_HEADER_LEN: usize = 16;
96
97/// What a code needs beside it: `norm`, `scale`, `lo` and `delta`, four `f32`.
98pub const META_LEN: usize = 16;
99
100/// What kind of index an image holds.
101///
102/// One value today and a byte for it, because the vector index is the first
103/// thing worth writing down and it will not be the last. A reader that meets a
104/// kind it does not know throws the image away and rebuilds, which is always
105/// available and is why this is a byte rather than a format version.
106pub mod image_kind {
107    /// The partition index over RaBitQ codes (`10`).
108    pub const VECTOR: u8 = 1;
109}
110
111/// How a vector is compared, as the byte the image stores.
112///
113/// The same four values `yo_shape::Metric` has, written down here because this
114/// crate is the format and it does not depend on the shape crate. A collection
115/// built for cosine and searched as L2 answers wrongly and quietly, so the
116/// metric travels with the image rather than being taken on trust from whatever
117/// opened it.
118pub mod metric {
119    /// Euclidean distance.
120    pub const L2: u8 = 0;
121    /// Cosine similarity, stored as unit vectors.
122    pub const COSINE: u8 = 1;
123    /// Inner product.
124    pub const IP: u8 = 2;
125    /// Hamming distance.
126    pub const HAMMING: u8 = 3;
127
128    /// Whether `b` is a metric this version has a name for.
129    #[must_use]
130    pub const fn is_known(b: u8) -> bool {
131        matches!(b, L2 | COSINE | IP | HAMMING)
132    }
133}
134
135/// The root of an image: everything the index is, apart from the members.
136///
137/// The two chains here and the partition table after it are addresses into the
138/// log, so this is small whatever the collection's size: a few dozen bytes plus
139/// sixteen per partition.
140#[derive(Debug, Clone, Copy, PartialEq, Default)]
141pub struct ImageHeader {
142    /// Which index this is an image of. See [`image_kind`].
143    pub kind: u8,
144    /// How wide one coordinate is written in a code, which is 1 or 4.
145    pub bits: u8,
146    /// What nearness means here. See [`metric`].
147    pub metric: u8,
148    /// How many coordinates a vector has.
149    pub dim: u32,
150    /// How many partitions the index has grown to.
151    pub partitions: u32,
152    /// What the rotation was built from. Without it the codes are noise.
153    pub seed: u64,
154    /// How many vectors the collection held, for a check after loading.
155    pub members: u64,
156    /// How many slots the vector table had, which is the largest id plus one.
157    ///
158    /// An id is a slot rather than a name, and a collection that has had
159    /// members removed has holes in it, so the count of live members does not
160    /// say how far the ids go. Writing it down means a loader allocates the
161    /// table once and can refuse an id that is past the end of it before it is
162    /// used to index anything.
163    pub slots: u32,
164    /// The size a partition wants.
165    pub posting: u32,
166    /// How many partitions a search scans.
167    pub probe: u32,
168    /// How many candidates are reranked per answer.
169    pub rerank: u32,
170    /// How many neighbours a split sweeps.
171    pub sweep: u32,
172    /// How much further a filtered search looks.
173    pub widen: u32,
174    /// How many partitions one vector may be written into.
175    ///
176    /// Part of the image rather than left to whatever the loader happens to be
177    /// tuned to, because it says how the collection was built. A collection
178    /// built with boundary copies and reopened without them keeps the copies it
179    /// has and stops making new ones, which is a recall curve that changes under
180    /// the caller for a reason nothing reports.
181    pub spill: u32,
182    /// How much further than the nearest centroid a copy is still made, as a
183    /// fraction.
184    pub slack: f32,
185    /// How many partitions in a row may add nothing before a search stops.
186    pub patience: u32,
187    /// Where the centroids are, and how many bytes of them there are.
188    pub centroids: Chain,
189    /// Where the key table is, and how long it is.
190    pub keys: Chain,
191}
192
193/// Where a section went and how long it is.
194///
195/// The same pair `yo_kv::cold::Chain` carries, written here as two `u64` because
196/// this crate is the layout and does not depend on the crate that walks the
197/// chunks.
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
199pub struct Chain {
200    /// The address of the single chunk, or of the directory.
201    pub at: u64,
202    /// The section's length in bytes, which is what says which of those it is.
203    pub len: u64,
204}
205
206/// How long an image root with this many partitions is.
207///
208/// # Errors
209///
210/// [`Code::Invalid`] if the partition count is large enough that the root would
211/// not fit in a `usize`, which on a 64 bit machine it never is and on a 32 bit
212/// one is a corrupt count rather than a real index.
213pub fn image_len(partitions: u32) -> Result<usize> {
214    (partitions as usize)
215        .checked_mul(PARTITION_ENTRY_LEN)
216        .and_then(|n| n.checked_add(IMAGE_HEADER_LEN))
217        .ok_or_else(|| {
218            Error::new(Code::Invalid, "that many partitions do not fit in an image")
219                .with_detail(format!("partitions={partitions}"))
220        })
221}
222
223impl ImageHeader {
224    /// Writes the header into the first [`IMAGE_HEADER_LEN`] bytes of `into`.
225    ///
226    /// # Errors
227    ///
228    /// [`Code::Invalid`] if `into` is shorter than a whole root, header and
229    /// partition table both, because a header written into a buffer that cannot
230    /// hold the table it describes is a root nobody can read.
231    pub fn encode(&self, into: &mut [u8]) -> Result<usize> {
232        let need = image_len(self.partitions)?;
233        if into.len() < need {
234            return Err(
235                Error::new(Code::Invalid, "buffer is shorter than the image root")
236                    .with_detail(format!("have={} need={need}", into.len())),
237            );
238        }
239        put_u32(into, 0, IMAGE_TAG);
240        put_u8(into, 4, self.kind);
241        put_u8(into, 5, self.bits);
242        put_u8(into, 6, self.metric);
243        put_u8(into, 7, 0);
244        put_u32(into, 8, self.dim);
245        put_u32(into, 12, self.partitions);
246        put_u64(into, 16, self.seed);
247        put_u64(into, 24, self.members);
248        put_u32(into, 32, self.posting);
249        put_u32(into, 36, self.probe);
250        put_u32(into, 40, self.rerank);
251        put_u32(into, 44, self.sweep);
252        put_u32(into, 48, self.widen);
253        put_u32(into, 52, self.slots);
254        put_u64(into, 56, self.centroids.at);
255        put_u64(into, 64, self.centroids.len);
256        put_u64(into, 72, self.keys.at);
257        put_u64(into, 80, self.keys.len);
258        put_u32(into, 88, self.spill);
259        put_f32(into, 92, self.slack);
260        put_u32(into, 96, self.patience);
261        Ok(need)
262    }
263
264    /// Reads a header back and checks that the bytes behind it are a whole
265    /// partition table.
266    ///
267    /// # Errors
268    ///
269    /// [`Code::Corrupt`] if the tag is wrong, if a reserved field is set, if the
270    /// dimension or the code width is not one this version writes, or if the
271    /// buffer is not as long as the partition count says it is.
272    pub fn decode(bytes: &[u8]) -> Result<ImageHeader> {
273        if bytes.len() < IMAGE_HEADER_LEN {
274            return Err(Error::new(Code::Corrupt, "shorter than an image header")
275                .with_detail(format!("len={}", bytes.len())));
276        }
277        let tag = get_u32(bytes, 0);
278        if tag != IMAGE_TAG {
279            return Err(Error::new(Code::Corrupt, "not an index image")
280                .with_detail(format!("tag={tag:#010x}")));
281        }
282        // Reserved bytes are checked rather than ignored, as everywhere else in
283        // this crate: anything in them was written by something that did not
284        // agree with this layout.
285        if get_u8(bytes, 7) != 0 {
286            return Err(Error::new(
287                Code::Corrupt,
288                "reserved image header bytes are set",
289            ));
290        }
291        let h = ImageHeader {
292            kind: get_u8(bytes, 4),
293            bits: get_u8(bytes, 5),
294            metric: get_u8(bytes, 6),
295            dim: get_u32(bytes, 8),
296            partitions: get_u32(bytes, 12),
297            seed: get_u64(bytes, 16),
298            members: get_u64(bytes, 24),
299            posting: get_u32(bytes, 32),
300            probe: get_u32(bytes, 36),
301            rerank: get_u32(bytes, 40),
302            sweep: get_u32(bytes, 44),
303            widen: get_u32(bytes, 48),
304            slots: get_u32(bytes, 52),
305            spill: get_u32(bytes, 88),
306            slack: get_f32(bytes, 92),
307            patience: get_u32(bytes, 96),
308            centroids: Chain {
309                at: get_u64(bytes, 56),
310                len: get_u64(bytes, 64),
311            },
312            keys: Chain {
313                at: get_u64(bytes, 72),
314                len: get_u64(bytes, 80),
315            },
316        };
317        if h.dim == 0 || h.dim as usize > crate::vector::MAX_DIM {
318            return Err(Error::new(Code::Corrupt, "image dimension out of range")
319                .with_detail(format!("dim={}", h.dim)));
320        }
321        if h.bits != 1 && h.bits != 4 {
322            return Err(Error::new(Code::Corrupt, "unknown code width")
323                .with_detail(format!("bits={}", h.bits)));
324        }
325        if h.members > u64::from(h.slots) {
326            return Err(
327                Error::new(Code::Corrupt, "more members than the table has slots")
328                    .with_detail(format!("members={} slots={}", h.members, h.slots)),
329            );
330        }
331        if !metric::is_known(h.metric) {
332            return Err(Error::new(Code::Corrupt, "unknown metric")
333                .with_detail(format!("metric={}", h.metric)));
334        }
335        let need = image_len(h.partitions)?;
336        if bytes.len() != need {
337            return Err(
338                Error::new(Code::Corrupt, "the image root is not the length it says")
339                    .with_detail(format!("len={} need={need}", bytes.len())),
340            );
341        }
342        // The centroids are the one section whose size the header already
343        // implies, so a disagreement there is worth catching before anything
344        // tries to cut the section into partitions.
345        let want = u64::from(h.partitions) * u64::from(h.dim) * 4;
346        if h.centroids.len != want {
347            return Err(
348                Error::new(Code::Corrupt, "the centroid section is the wrong size")
349                    .with_detail(format!("len={} want={want}", h.centroids.len)),
350            );
351        }
352        Ok(h)
353    }
354}
355
356/// Writes partition `i`'s chain into an encoded root.
357///
358/// # Errors
359///
360/// [`Code::Invalid`] if `i` is past the table.
361pub fn put_partition(root: &mut [u8], i: u32, chain: Chain) -> Result<()> {
362    let at =
363        partition_offset(root.len(), i).ok_or_else(|| missing(i, root.len(), Code::Invalid))?;
364    put_u64(root, at, chain.at);
365    put_u64(root, at + 8, chain.len);
366    Ok(())
367}
368
369/// Reads partition `i`'s chain back out of an encoded root.
370///
371/// # Errors
372///
373/// [`Code::Corrupt`] if `i` is past the table.
374pub fn get_partition(root: &[u8], i: u32) -> Result<Chain> {
375    let at =
376        partition_offset(root.len(), i).ok_or_else(|| missing(i, root.len(), Code::Corrupt))?;
377    Ok(Chain {
378        at: get_u64(root, at),
379        len: get_u64(root, at + 8),
380    })
381}
382
383fn partition_offset(root_len: usize, i: u32) -> Option<usize> {
384    let at = IMAGE_HEADER_LEN + (i as usize) * PARTITION_ENTRY_LEN;
385    (at + PARTITION_ENTRY_LEN <= root_len).then_some(at)
386}
387
388/// A partition the table does not have, which is a caller's mistake on the way
389/// in and a broken root on the way out.
390fn missing(i: u32, root_len: usize, code: Code) -> Error {
391    Error::new(code, "no such partition in the image")
392        .with_detail(format!("partition={i} root={root_len}"))
393}
394
395/// The fixed part at the front of one partition's image.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
397pub struct PostingHeader {
398    /// How many members the partition holds.
399    pub count: u32,
400    /// How many bytes one code takes.
401    ///
402    /// Derivable from the root's `dim` and `bits`, and stored anyway for the
403    /// same reason `dim` is stored in a vector record: a partition can then be
404    /// cut into its four arrays without the root, which is what a reader
405    /// checking a file rather than opening one has.
406    pub code_bytes: u32,
407    /// The size at which a split was tried and found there was no cut.
408    ///
409    /// Part of the image because it is not derivable: it is the memory of a
410    /// split that failed, and an image that dropped it would have every
411    /// partition of identical vectors try to split again on the first write
412    /// after an open.
413    pub stuck: u32,
414}
415
416/// How long one partition's image is.
417///
418/// # Errors
419///
420/// [`Code::Invalid`] if the arithmetic overflows, which means a corrupt count
421/// rather than a real partition.
422pub fn posting_len(count: u32, code_bytes: u32) -> Result<usize> {
423    let count = count as usize;
424    let per = 8usize
425        .checked_add(8)
426        .and_then(|n| n.checked_add(code_bytes as usize))
427        .and_then(|n| n.checked_add(META_LEN))
428        .ok_or_else(|| Error::new(Code::Invalid, "a member of that size does not fit"))?;
429    count
430        .checked_mul(per)
431        .and_then(|n| n.checked_add(POSTING_HEADER_LEN))
432        .ok_or_else(|| {
433            Error::new(Code::Invalid, "that many members do not fit in a partition")
434                .with_detail(format!("count={count} code_bytes={code_bytes}"))
435        })
436}
437
438impl PostingHeader {
439    /// Writes the header into the front of `into`.
440    ///
441    /// # Errors
442    ///
443    /// [`Code::Invalid`] if `into` is not long enough for the whole partition.
444    pub fn encode(&self, into: &mut [u8]) -> Result<usize> {
445        let need = posting_len(self.count, self.code_bytes)?;
446        if into.len() < need {
447            return Err(
448                Error::new(Code::Invalid, "buffer is shorter than the posting")
449                    .with_detail(format!("have={} need={need}", into.len())),
450            );
451        }
452        put_u32(into, 0, self.count);
453        put_u32(into, 4, self.code_bytes);
454        put_u32(into, 8, self.stuck);
455        put_u32(into, 12, 0);
456        Ok(need)
457    }
458
459    /// Reads a partition's header back and checks the bytes behind it.
460    ///
461    /// # Errors
462    ///
463    /// [`Code::Corrupt`] if a reserved field is set or if the buffer is not
464    /// exactly the length the header describes.
465    pub fn decode(bytes: &[u8]) -> Result<PostingHeader> {
466        if bytes.len() < POSTING_HEADER_LEN {
467            return Err(Error::new(Code::Corrupt, "shorter than a posting header")
468                .with_detail(format!("len={}", bytes.len())));
469        }
470        if get_u32(bytes, 12) != 0 {
471            return Err(Error::new(
472                Code::Corrupt,
473                "reserved posting header bytes are set",
474            ));
475        }
476        let h = PostingHeader {
477            count: get_u32(bytes, 0),
478            code_bytes: get_u32(bytes, 4),
479            stuck: get_u32(bytes, 8),
480        };
481        let Ok(need) = posting_len(h.count, h.code_bytes) else {
482            return Err(
483                Error::new(Code::Corrupt, "that many members do not fit in a partition")
484                    .with_detail(format!("count={} code_bytes={}", h.count, h.code_bytes)),
485            );
486        };
487        if bytes.len() != need {
488            return Err(
489                Error::new(Code::Corrupt, "the posting is not the length it says")
490                    .with_detail(format!("len={} need={need}", bytes.len())),
491            );
492        }
493        Ok(h)
494    }
495
496    /// Where the ids start.
497    #[must_use]
498    pub const fn ids_at(&self) -> usize {
499        POSTING_HEADER_LEN
500    }
501
502    /// Where the tags start.
503    #[must_use]
504    pub const fn tags_at(&self) -> usize {
505        self.ids_at() + self.count as usize * 8
506    }
507
508    /// Where the codes start.
509    #[must_use]
510    pub const fn codes_at(&self) -> usize {
511        self.tags_at() + self.count as usize * 8
512    }
513
514    /// Where the meta starts.
515    #[must_use]
516    pub const fn meta_at(&self) -> usize {
517        self.codes_at() + self.count as usize * self.code_bytes as usize
518    }
519}
520
521/// Writes floats into `into` end to end and says how many bytes that took.
522///
523/// Every float in an image goes through here: the centroid section is one long
524/// run of them, and the four numbers beside a code are a run of four. Both are
525/// bit for bit, because a centroid that comes back nearly right puts members
526/// under the wrong partition and a code's scale that comes back nearly right
527/// reorders the answers.
528///
529/// # Errors
530///
531/// [`Code::Invalid`] if `into` is too short.
532pub fn put_floats(into: &mut [u8], values: &[f32]) -> Result<usize> {
533    let need = values.len() * 4;
534    if into.len() < need {
535        return Err(
536            Error::new(Code::Invalid, "buffer is shorter than the floats")
537                .with_detail(format!("have={} need={need}", into.len())),
538        );
539    }
540    for (i, v) in values.iter().enumerate() {
541        crate::put_f32(into, i * 4, *v);
542    }
543    Ok(need)
544}
545
546/// Reads floats back out of `bytes` into `out`, which says how many.
547///
548/// # Errors
549///
550/// [`Code::Corrupt`] if there are not that many floats there, which is what a
551/// section that disagrees with the header it was described by looks like.
552pub fn get_floats(bytes: &[u8], out: &mut [f32]) -> Result<()> {
553    let need = out.len() * 4;
554    if bytes.len() < need {
555        return Err(
556            Error::new(Code::Corrupt, "the section is shorter than its floats")
557                .with_detail(format!("len={} need={need}", bytes.len())),
558        );
559    }
560    for (i, slot) in out.iter_mut().enumerate() {
561        *slot = crate::get_f32(bytes, i * 4);
562    }
563    Ok(())
564}
565
566/// How many bytes a key of `klen` takes in the key table.
567///
568/// # Errors
569///
570/// [`Code::Invalid`] if the key is longer than a record's key can be, which is
571/// the same limit for the same reason: a key is a thing you look up by.
572pub fn key_entry_len(klen: usize) -> Result<usize> {
573    if klen > crate::record::MAX_KEY_LEN {
574        return Err(
575            Error::new(Code::Invalid, "the key is longer than 65535 bytes")
576                .with_detail(format!("klen={klen}")),
577        );
578    }
579    Ok(10 + klen)
580}
581
582/// Writes one key table entry and says how long it was.
583///
584/// The table is a run of `id`, `klen`, key, with no padding and no order worth
585/// relying on, because the only thing that reads it reads all of it. A key is
586/// tens of bytes and the alignment would cost more than the sequential read
587/// saves.
588///
589/// # Errors
590///
591/// [`Code::Invalid`] if the key is too long or `into` is too short.
592pub fn put_key(into: &mut [u8], id: u64, key: &[u8]) -> Result<usize> {
593    let need = key_entry_len(key.len())?;
594    if into.len() < need {
595        return Err(
596            Error::new(Code::Invalid, "buffer is shorter than the key entry")
597                .with_detail(format!("have={} need={need}", into.len())),
598        );
599    }
600    put_u64(into, 0, id);
601    put_u16(into, 8, key.len() as u16);
602    into[10..need].copy_from_slice(key);
603    Ok(need)
604}
605
606/// The key table, one entry at a time.
607///
608/// Stops at the first entry that does not fit, which is what a truncated
609/// section looks like, and the caller compares the count it got against the
610/// member count in the header rather than being told twice.
611#[derive(Debug, Clone)]
612pub struct Keys<'a> {
613    rest: &'a [u8],
614}
615
616impl<'a> Keys<'a> {
617    /// Walks the entries in `bytes`.
618    #[must_use]
619    pub const fn new(bytes: &'a [u8]) -> Keys<'a> {
620        Keys { rest: bytes }
621    }
622
623    /// Whether every byte handed in was accounted for.
624    ///
625    /// False after a short entry, which is the one thing walking the table
626    /// cannot tell a caller by ending.
627    #[must_use]
628    pub const fn done(&self) -> bool {
629        self.rest.is_empty()
630    }
631}
632
633impl<'a> Iterator for Keys<'a> {
634    type Item = (u64, &'a [u8]);
635
636    fn next(&mut self) -> Option<(u64, &'a [u8])> {
637        if self.rest.len() < 10 {
638            return None;
639        }
640        let id = get_u64(self.rest, 0);
641        let klen = get_u16(self.rest, 8) as usize;
642        let end = 10 + klen;
643        if self.rest.len() < end {
644            return None;
645        }
646        let key = &self.rest[10..end];
647        self.rest = &self.rest[end..];
648        Some((id, key))
649    }
650}
651
652#[cfg(test)]
653mod tests {
654    use super::*;
655
656    fn header(partitions: u32, dim: u32) -> ImageHeader {
657        ImageHeader {
658            kind: image_kind::VECTOR,
659            bits: 1,
660            metric: metric::COSINE,
661            dim,
662            partitions,
663            seed: 0x0102_0304_0506_0708,
664            members: 9999,
665            slots: 10_000,
666            posting: 256,
667            probe: 8,
668            rerank: 4,
669            sweep: 4,
670            widen: 8,
671            spill: 4,
672            slack: 0.10,
673            patience: 3,
674            centroids: Chain {
675                at: 4096,
676                len: u64::from(partitions) * u64::from(dim) * 4,
677            },
678            keys: Chain { at: 8192, len: 123 },
679        }
680    }
681
682    #[test]
683    fn a_root_comes_back_field_for_field() {
684        let h = header(3, 128);
685        let mut buf = vec![0u8; image_len(3).unwrap()];
686        let wrote = h.encode(&mut buf).unwrap();
687        assert_eq!(wrote, buf.len());
688        assert_eq!(ImageHeader::decode(&buf).unwrap(), h);
689    }
690
691    #[test]
692    fn the_partition_table_is_addressed_and_not_walked() {
693        let h = header(4, 8);
694        let mut buf = vec![0u8; image_len(4).unwrap()];
695        h.encode(&mut buf).unwrap();
696        for i in 0..4 {
697            let chain = Chain {
698                at: 1000 + u64::from(i),
699                len: 64 + u64::from(i),
700            };
701            put_partition(&mut buf, i, chain).unwrap();
702        }
703        for i in 0..4 {
704            assert_eq!(
705                get_partition(&buf, i).unwrap(),
706                Chain {
707                    at: 1000 + u64::from(i),
708                    len: 64 + u64::from(i)
709                }
710            );
711        }
712        assert!(get_partition(&buf, 4).is_err(), "there is no fifth");
713        assert!(put_partition(&mut buf, 9, Chain::default()).is_err());
714    }
715
716    #[test]
717    fn a_root_that_is_not_a_root_is_refused() {
718        let h = header(2, 16);
719        let mut buf = vec![0u8; image_len(2).unwrap()];
720        h.encode(&mut buf).unwrap();
721        assert!(ImageHeader::decode(&buf).is_ok());
722
723        let mut wrong = buf.clone();
724        put_u32(&mut wrong, 0, 0xdead_beef);
725        assert_eq!(
726            ImageHeader::decode(&wrong).unwrap_err().code(),
727            Code::Corrupt,
728            "a chunk that is not an image was read as one"
729        );
730
731        let mut set = buf.clone();
732        set[7] = 1;
733        assert!(
734            ImageHeader::decode(&set).is_err(),
735            "byte 7 is reserved and a writer that set it disagreed with this layout"
736        );
737
738        let mut short = buf.clone();
739        put_u32(&mut short, 52, 3);
740        assert!(
741            ImageHeader::decode(&short).is_err(),
742            "a table with fewer slots than members cannot hold them"
743        );
744
745        let mut bits = buf.clone();
746        put_u8(&mut bits, 5, 2);
747        assert!(ImageHeader::decode(&bits).is_err(), "no two bit codes");
748
749        let mut met = buf.clone();
750        put_u8(&mut met, 6, 9);
751        assert!(ImageHeader::decode(&met).is_err(), "no ninth metric");
752
753        let mut dim = buf.clone();
754        put_u32(&mut dim, 8, 0);
755        assert!(ImageHeader::decode(&dim).is_err(), "no zero dimension");
756
757        // The count and the buffer have to agree, because a count that is
758        // believed on its own is a count that indexes past the table.
759        let mut count = buf.clone();
760        put_u32(&mut count, 12, 99);
761        assert!(ImageHeader::decode(&count).is_err());
762
763        let mut cent = buf.clone();
764        put_u64(&mut cent, 64, 7);
765        assert!(
766            ImageHeader::decode(&cent).is_err(),
767            "the centroid section has to be partitions times dim floats"
768        );
769
770        for len in 0..buf.len() {
771            assert!(
772                ImageHeader::decode(&buf[..len]).is_err(),
773                "{len} bytes decoded as a two partition image"
774            );
775        }
776    }
777
778    #[test]
779    fn a_partition_is_four_runs_that_do_not_overlap() {
780        let h = PostingHeader {
781            count: 5,
782            code_bytes: 16,
783            stuck: 12,
784        };
785        let mut buf = vec![0u8; posting_len(5, 16).unwrap()];
786        let wrote = h.encode(&mut buf).unwrap();
787        assert_eq!(wrote, buf.len());
788        assert_eq!(PostingHeader::decode(&buf).unwrap(), h);
789
790        assert_eq!(h.ids_at(), POSTING_HEADER_LEN);
791        assert_eq!(h.tags_at(), h.ids_at() + 40);
792        assert_eq!(h.codes_at(), h.tags_at() + 40);
793        assert_eq!(h.meta_at(), h.codes_at() + 80);
794        assert_eq!(h.meta_at() + 5 * META_LEN, buf.len());
795    }
796
797    #[test]
798    fn an_empty_partition_is_a_header_and_nothing_else() {
799        let h = PostingHeader {
800            count: 0,
801            code_bytes: 96,
802            stuck: 0,
803        };
804        let mut buf = vec![0u8; POSTING_HEADER_LEN];
805        h.encode(&mut buf).unwrap();
806        assert_eq!(PostingHeader::decode(&buf).unwrap(), h);
807        assert_eq!(h.meta_at(), buf.len());
808    }
809
810    #[test]
811    fn a_posting_that_is_not_the_length_it_claims_is_refused() {
812        let h = PostingHeader {
813            count: 3,
814            code_bytes: 8,
815            stuck: 0,
816        };
817        let mut buf = vec![0u8; posting_len(3, 8).unwrap()];
818        h.encode(&mut buf).unwrap();
819        for len in 0..buf.len() {
820            assert!(
821                PostingHeader::decode(&buf[..len]).is_err(),
822                "{len} bytes decoded as three members"
823            );
824        }
825        let mut set = buf.clone();
826        set[12] = 1;
827        assert!(PostingHeader::decode(&set).is_err(), "reserved");
828
829        // A count nobody could have written, which is what a corrupt header
830        // looks like, and it has to be refused before anything multiplies it out.
831        let mut huge = buf.clone();
832        put_u32(&mut huge, 0, u32::MAX);
833        put_u32(&mut huge, 4, u32::MAX);
834        assert!(PostingHeader::decode(&huge).is_err());
835    }
836
837    #[test]
838    fn floats_go_down_and_come_back_bit_for_bit() {
839        let values = [0.0f32, -0.0, 1.5, -2.25, 1e-38, 3.4e38];
840        let mut buf = vec![0u8; values.len() * 4];
841        assert_eq!(put_floats(&mut buf, &values).unwrap(), buf.len());
842        let mut back = vec![0f32; values.len()];
843        get_floats(&buf, &mut back).unwrap();
844        for (a, b) in values.iter().zip(&back) {
845            assert_eq!(a.to_bits(), b.to_bits(), "{a} came back as {b}");
846        }
847        assert!(get_floats(&buf[..4], &mut back).is_err(), "not that many");
848        assert!(put_floats(&mut buf[..4], &values).is_err());
849    }
850
851    #[test]
852    fn the_key_table_walks_back_in_order() {
853        let entries: Vec<(u64, &[u8])> = vec![
854            (0, b"a".as_slice()),
855            (7, b"".as_slice()),
856            (3, b"a rather longer key than the first one".as_slice()),
857        ];
858        let mut buf = Vec::new();
859        for (id, key) in &entries {
860            let mut one = vec![0u8; key_entry_len(key.len()).unwrap()];
861            let wrote = put_key(&mut one, *id, key).unwrap();
862            assert_eq!(wrote, one.len());
863            buf.extend_from_slice(&one);
864        }
865        let mut walk = Keys::new(&buf);
866        let got: Vec<(u64, &[u8])> = walk.by_ref().collect();
867        assert_eq!(got, entries);
868        assert!(walk.done(), "the walk left bytes behind");
869    }
870
871    #[test]
872    fn a_truncated_key_table_stops_rather_than_reading_past_it() {
873        let mut buf = vec![0u8; key_entry_len(4).unwrap()];
874        put_key(&mut buf, 1, b"abcd").unwrap();
875        assert!(Keys::new(&[]).done(), "no bytes is an empty table");
876        for len in 1..buf.len() {
877            let mut walk = Keys::new(&buf[..len]);
878            assert_eq!(walk.by_ref().count(), 0, "{len} bytes gave a whole key");
879            assert!(!walk.done(), "a short entry is not a finished table");
880        }
881        assert!(key_entry_len(70_000).is_err());
882    }
883
884    #[test]
885    fn the_layout_is_the_one_written_down() {
886        // The numbers in the module diagram, so that a change to any of them is
887        // a change to a test rather than a silent change to the format.
888        assert_eq!(IMAGE_HEADER_LEN, 100);
889        assert_eq!(PARTITION_ENTRY_LEN, 16);
890        assert_eq!(POSTING_HEADER_LEN, 16);
891        assert_eq!(META_LEN, 16);
892        assert_eq!(IMAGE_TAG, u32::from_le_bytes(*b"YOIX"));
893        assert_eq!(image_len(0).unwrap(), IMAGE_HEADER_LEN);
894        assert_eq!(image_len(1).unwrap(), IMAGE_HEADER_LEN + 16);
895        // A partition at the default posting size and 768 dimensions, which is
896        // the case the chunk size was chosen for.
897        assert!(
898            posting_len(256, 96).unwrap() < 64 * 1024,
899            "a partition should be one chunk at the sizes it is tuned for"
900        );
901    }
902}