Skip to main content

hermes_core/structures/vector/scann/
artifact.rs

1use super::{MAX_SCANN_TREE_LEVELS, ScannConfig, ScannEncoding, ScannFormatError, ScannResult};
2use std::io::Write;
3use std::ops::Range;
4
5const MAGIC: &[u8; 8] = b"HSCNGLOB";
6pub const SCANN_GLOBAL_ARTIFACT_VERSION: u16 = 1;
7const FINGERPRINT_OFFSET: usize = 12;
8
9/// One routing centroid level. Float centroids are stored as per-dimension
10/// fixed-point bytes with `minimums` and `steps`; binary centroids are their
11/// exact packed bytes and leave both quantization vectors empty.
12#[derive(Debug, Clone, PartialEq)]
13pub struct ScannRoutingLevel {
14    pub centroid_count: u32,
15    pub centroid_codes: Vec<u8>,
16    pub minimums: Vec<f32>,
17    pub steps: Vec<f32>,
18    /// CSR offsets from this level into the next. Empty only on the leaf level.
19    pub child_offsets: Vec<u32>,
20}
21
22/// Global residual AH codebook. Binary Hamming ScaNN intentionally has no AH
23/// codebook because the segment leaves retain exact packed vectors.
24#[derive(Debug, Clone, PartialEq)]
25pub struct ScannAhCodebook {
26    pub dimensions_per_block: u16,
27    pub centers_per_block: u16,
28    /// Block-major, center-major, coordinate-major f32 values.
29    pub centers: Vec<f32>,
30}
31
32/// A single trained routing/codebook generation shared by every segment.
33#[derive(Debug, Clone, PartialEq)]
34pub struct ScannTrainedArtifact {
35    pub generation: u64,
36    pub artifact_id: u64,
37    pub trained_vectors: u64,
38    pub config: ScannConfig,
39    pub levels: Vec<ScannRoutingLevel>,
40    pub ah_codebook: Option<ScannAhCodebook>,
41}
42
43#[derive(Debug, Clone)]
44struct ScannRoutingLevelRange {
45    centroid_count: u32,
46    centroid_codes: Range<usize>,
47    minimums: Range<usize>,
48    steps: Range<usize>,
49    child_offsets: Range<usize>,
50}
51
52#[derive(Debug, Clone)]
53struct ScannAhCodebookRange {
54    dimensions_per_block: u16,
55    centers_per_block: u16,
56    centers: Range<usize>,
57}
58
59/// Borrowed, zero-copy view of a persisted global ScaNN generation.
60///
61/// The routing centroid plane can be many gigabytes at billion-vector scale.
62/// Parsing retains checked ranges into the caller's mmap-backed slice and
63/// allocates only the at-most-three small level descriptors.
64#[derive(Debug, Clone)]
65pub struct ScannTrainedArtifactView<'a> {
66    bytes: &'a [u8],
67    pub generation: u64,
68    pub artifact_id: u64,
69    pub trained_vectors: u64,
70    pub config: ScannConfig,
71    levels: Vec<ScannRoutingLevelRange>,
72    ah_codebook: Option<ScannAhCodebookRange>,
73}
74
75#[derive(Debug, Clone, Copy)]
76pub struct ScannRoutingLevelRef<'a> {
77    pub centroid_count: u32,
78    pub centroid_codes: &'a [u8],
79    minimums_le: &'a [u8],
80    steps_le: &'a [u8],
81    child_offsets_le: &'a [u8],
82}
83
84impl<'a> ScannRoutingLevelRef<'a> {
85    pub fn minimums(&self) -> impl ExactSizeIterator<Item = f32> + 'a {
86        self.minimums_le
87            .chunks_exact(4)
88            .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
89    }
90
91    pub fn steps(&self) -> impl ExactSizeIterator<Item = f32> + 'a {
92        self.steps_le
93            .chunks_exact(4)
94            .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
95    }
96
97    pub fn child_offsets(&self) -> impl ExactSizeIterator<Item = u32> + 'a {
98        self.child_offsets_le
99            .chunks_exact(4)
100            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
101    }
102}
103
104#[derive(Debug, Clone, Copy)]
105pub struct ScannAhCodebookRef<'a> {
106    pub dimensions_per_block: u16,
107    pub centers_per_block: u16,
108    centers_le: &'a [u8],
109}
110
111impl<'a> ScannAhCodebookRef<'a> {
112    pub fn centers(&self) -> impl ExactSizeIterator<Item = f32> + 'a {
113        self.centers_le
114            .chunks_exact(4)
115            .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
116    }
117}
118
119impl<'a> ScannTrainedArtifactView<'a> {
120    pub fn parse(bytes: &'a [u8]) -> ScannResult<Self> {
121        let mut input = Input::new(bytes);
122        if input.take(8)? != MAGIC {
123            return Err(ScannFormatError::new("invalid ScaNN global artifact magic"));
124        }
125        let version = input.u16()?;
126        if version != SCANN_GLOBAL_ARTIFACT_VERSION {
127            return Err(ScannFormatError::new(format!(
128                "unsupported ScaNN global artifact version {version}; reader supports {SCANN_GLOBAL_ARTIFACT_VERSION}"
129            )));
130        }
131        if input.u16()? != 0 {
132            return Err(ScannFormatError::new(
133                "ScaNN global artifact reserved field is non-zero",
134            ));
135        }
136        let artifact_id = input.u64()?;
137        let generation = input.u64()?;
138        let trained_vectors = input.u64()?;
139        let dimension = input.u32()?;
140        let tree_levels = input.u8()?;
141        let encoding_tag = input.u8()?;
142        let dimensions_per_block = input.u16()?;
143        let bits_per_code = input.u8()?;
144        if input.take(3)? != [0, 0, 0] {
145            return Err(ScannFormatError::new(
146                "ScaNN global artifact reserved bytes are non-zero",
147            ));
148        }
149        let config = ScannConfig {
150            dimension,
151            tree_levels,
152            num_leaves: input.u32()?,
153            encoding: ScannEncoding::from_parts(encoding_tag, dimensions_per_block, bits_per_code)?,
154        };
155        config.validate()?;
156        let required = config.effective_training_threshold()?;
157        if generation == 0 || artifact_id == 0 || trained_vectors < required {
158            return Err(ScannFormatError::new(format!(
159                "invalid ScaNN generation metadata: generation={generation}, fingerprint={artifact_id}, trained={trained_vectors}, required={required}"
160            )));
161        }
162        let level_count = input.u8()?;
163        if input.take(7)? != [0; 7]
164            || level_count != config.tree_levels
165            || level_count > MAX_SCANN_TREE_LEVELS
166        {
167            return Err(ScannFormatError::new(
168                "ScaNN routing level count does not match configuration",
169            ));
170        }
171        let centroid_width = match config.encoding {
172            ScannEncoding::AsymmetricHash { .. } => config.dimension as usize,
173            ScannEncoding::BinaryHamming => config.dimension as usize / 8,
174        };
175        let mut levels = Vec::with_capacity(usize::from(level_count));
176        for level_index in 0..level_count {
177            let centroid_count = input.u32()?;
178            let code_len = input.usize()?;
179            let minimum_count = input.usize()?;
180            let step_count = input.usize()?;
181            let child_count = input.usize()?;
182            let expected_codes = (centroid_count as usize)
183                .checked_mul(centroid_width)
184                .ok_or_else(|| ScannFormatError::new("ScaNN centroid matrix size overflows"))?;
185            if centroid_count == 0 || code_len != expected_codes {
186                return Err(ScannFormatError::new(format!(
187                    "invalid ScaNN centroid matrix at level {level_index}"
188                )));
189            }
190            match config.encoding {
191                ScannEncoding::AsymmetricHash { .. }
192                    if minimum_count != config.dimension as usize
193                        || step_count != config.dimension as usize =>
194                {
195                    return Err(ScannFormatError::new(format!(
196                        "invalid ScaNN fixed-point parameters at level {level_index}"
197                    )));
198                }
199                ScannEncoding::BinaryHamming if minimum_count != 0 || step_count != 0 => {
200                    return Err(ScannFormatError::new(
201                        "binary ScaNN centroids must not carry float quantization parameters",
202                    ));
203                }
204                _ => {}
205            }
206            let centroid_codes = input.take_range(code_len)?;
207            let minimums = input.take_range(checked_word_bytes(minimum_count)?)?;
208            let steps = input.take_range(checked_word_bytes(step_count)?)?;
209            let child_offsets = input.take_range(checked_word_bytes(child_count)?)?;
210            if bytes[minimums.clone()]
211                .chunks_exact(4)
212                .chain(bytes[steps.clone()].chunks_exact(4))
213                .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
214                .any(|value| !value.is_finite())
215            {
216                return Err(ScannFormatError::new(format!(
217                    "non-finite ScaNN fixed-point parameter at level {level_index}"
218                )));
219            }
220            levels.push(ScannRoutingLevelRange {
221                centroid_count,
222                centroid_codes,
223                minimums,
224                steps,
225                child_offsets,
226            });
227        }
228        validate_borrowed_levels(&config, &levels, bytes)?;
229        let ah_codebook = match input.u8()? {
230            0 => None,
231            1 => {
232                let dimensions_per_block = input.u16()?;
233                let centers_per_block = input.u16()?;
234                let center_count = input.usize()?;
235                let centers = input.take_range(checked_word_bytes(center_count)?)?;
236                if bytes[centers.clone()]
237                    .chunks_exact(4)
238                    .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
239                    .any(|value| !value.is_finite())
240                {
241                    return Err(ScannFormatError::new(
242                        "ScaNN AH codebook contains non-finite values",
243                    ));
244                }
245                Some(ScannAhCodebookRange {
246                    dimensions_per_block,
247                    centers_per_block,
248                    centers,
249                })
250            }
251            _ => {
252                return Err(ScannFormatError::new(
253                    "invalid ScaNN AH codebook presence tag",
254                ));
255            }
256        };
257        if !input.is_empty() {
258            return Err(ScannFormatError::new(
259                "ScaNN global artifact has trailing bytes",
260            ));
261        }
262        validate_borrowed_codebook(&config, ah_codebook.as_ref(), bytes)?;
263        if fingerprint(bytes) != artifact_id {
264            return Err(ScannFormatError::new(
265                "ScaNN global artifact fingerprint mismatch",
266            ));
267        }
268        Ok(Self {
269            bytes,
270            generation,
271            artifact_id,
272            trained_vectors,
273            config,
274            levels,
275            ah_codebook,
276        })
277    }
278
279    pub fn bytes(&self) -> &'a [u8] {
280        self.bytes
281    }
282
283    pub fn level_count(&self) -> usize {
284        self.levels.len()
285    }
286
287    pub fn level(&self, index: usize) -> Option<ScannRoutingLevelRef<'a>> {
288        let level = self.levels.get(index)?;
289        Some(ScannRoutingLevelRef {
290            centroid_count: level.centroid_count,
291            centroid_codes: &self.bytes[level.centroid_codes.clone()],
292            minimums_le: &self.bytes[level.minimums.clone()],
293            steps_le: &self.bytes[level.steps.clone()],
294            child_offsets_le: &self.bytes[level.child_offsets.clone()],
295        })
296    }
297
298    /// Checked byte range of one quantized centroid plane. Executable mmap
299    /// models retain this descriptor rather than decoding the plane to f32.
300    pub fn level_centroid_codes_range(&self, index: usize) -> Option<Range<usize>> {
301        self.levels
302            .get(index)
303            .map(|level| level.centroid_codes.clone())
304    }
305
306    pub fn ah_codebook(&self) -> Option<ScannAhCodebookRef<'a>> {
307        self.ah_codebook
308            .as_ref()
309            .map(|codebook| ScannAhCodebookRef {
310                dimensions_per_block: codebook.dimensions_per_block,
311                centers_per_block: codebook.centers_per_block,
312                centers_le: &self.bytes[codebook.centers.clone()],
313            })
314    }
315}
316
317fn validate_borrowed_levels(
318    config: &ScannConfig,
319    levels: &[ScannRoutingLevelRange],
320    bytes: &[u8],
321) -> ScannResult<()> {
322    for (level_index, level) in levels.iter().enumerate() {
323        let is_leaf = level_index + 1 == levels.len();
324        if is_leaf {
325            if !level.child_offsets.is_empty() || level.centroid_count != config.num_leaves {
326                return Err(ScannFormatError::new(
327                    "ScaNN leaf level does not match configured leaves",
328                ));
329            }
330            continue;
331        }
332        let offsets = bytes[level.child_offsets.clone()]
333            .chunks_exact(4)
334            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()));
335        let mut previous = None;
336        let mut count = 0usize;
337        let mut last = 0u32;
338        for offset in offsets {
339            if previous.is_some_and(|value| value > offset) {
340                return Err(ScannFormatError::new(format!(
341                    "invalid ScaNN child directory at level {level_index}"
342                )));
343            }
344            previous = Some(offset);
345            last = offset;
346            count += 1;
347        }
348        if count != level.centroid_count as usize + 1
349            || previous.is_none()
350            || bytes[level.child_offsets.clone()].get(..4) != Some(&0u32.to_le_bytes())
351            || last != levels[level_index + 1].centroid_count
352        {
353            return Err(ScannFormatError::new(format!(
354                "invalid ScaNN child directory at level {level_index}"
355            )));
356        }
357    }
358    Ok(())
359}
360
361fn validate_borrowed_codebook(
362    config: &ScannConfig,
363    codebook: Option<&ScannAhCodebookRange>,
364    bytes: &[u8],
365) -> ScannResult<()> {
366    match (config.encoding, codebook) {
367        (
368            ScannEncoding::AsymmetricHash {
369                dimensions_per_block,
370                bits_per_code,
371            },
372            Some(codebook),
373        ) => {
374            let centers_per_block = 1usize << bits_per_code;
375            let blocks = (config.dimension as usize).div_ceil(usize::from(dimensions_per_block));
376            let expected_values = blocks
377                .checked_mul(centers_per_block)
378                .and_then(|count| count.checked_mul(usize::from(dimensions_per_block)))
379                .ok_or_else(|| ScannFormatError::new("ScaNN AH codebook size overflows"))?;
380            if codebook.dimensions_per_block != dimensions_per_block
381                || usize::from(codebook.centers_per_block) != centers_per_block
382                || bytes[codebook.centers.clone()].len() != checked_word_bytes(expected_values)?
383            {
384                return Err(ScannFormatError::new("invalid ScaNN AH codebook shape"));
385            }
386        }
387        (ScannEncoding::BinaryHamming, None) => {}
388        (ScannEncoding::AsymmetricHash { .. }, None) => {
389            return Err(ScannFormatError::new(
390                "float ScaNN artifact is missing its AH codebook",
391            ));
392        }
393        (ScannEncoding::BinaryHamming, Some(_)) => {
394            return Err(ScannFormatError::new(
395                "binary ScaNN must keep exact codes and has no AH codebook",
396            ));
397        }
398    }
399    Ok(())
400}
401
402fn checked_word_bytes(count: usize) -> ScannResult<usize> {
403    count
404        .checked_mul(4)
405        .ok_or_else(|| ScannFormatError::new("ScaNN word byte length overflows"))
406}
407
408impl ScannTrainedArtifact {
409    pub fn new(
410        generation: u64,
411        trained_vectors: u64,
412        config: ScannConfig,
413        levels: Vec<ScannRoutingLevel>,
414        ah_codebook: Option<ScannAhCodebook>,
415    ) -> ScannResult<Self> {
416        let mut artifact = Self {
417            generation,
418            artifact_id: 0,
419            trained_vectors,
420            config,
421            levels,
422            ah_codebook,
423        };
424        artifact.validate_shape()?;
425        artifact.artifact_id = artifact.compute_fingerprint()?;
426        Ok(artifact)
427    }
428
429    pub fn to_bytes(&self) -> ScannResult<Vec<u8>> {
430        self.validate()?;
431        self.encode(self.artifact_id)
432    }
433
434    /// Stream the artifact without materializing a second full-size byte Vec.
435    pub fn write_to(&self, writer: &mut impl Write) -> ScannResult<u64> {
436        self.validate()?;
437        let mut written = 0u64;
438        self.for_each_encoded_chunk(self.artifact_id, |chunk| {
439            writer.write_all(chunk).map_err(|error| {
440                ScannFormatError::new(format!("failed to write ScaNN artifact: {error}"))
441            })?;
442            written = written
443                .checked_add(chunk.len() as u64)
444                .ok_or_else(|| ScannFormatError::new("ScaNN artifact size exceeds u64"))?;
445            Ok(())
446        })?;
447        Ok(written)
448    }
449
450    pub fn from_bytes(bytes: &[u8]) -> ScannResult<Self> {
451        let mut input = Input::new(bytes);
452        if input.take(8)? != MAGIC {
453            return Err(ScannFormatError::new("invalid ScaNN global artifact magic"));
454        }
455        let version = input.u16()?;
456        if version != SCANN_GLOBAL_ARTIFACT_VERSION {
457            return Err(ScannFormatError::new(format!(
458                "unsupported ScaNN global artifact version {version}; reader supports {SCANN_GLOBAL_ARTIFACT_VERSION}"
459            )));
460        }
461        if input.u16()? != 0 {
462            return Err(ScannFormatError::new(
463                "ScaNN global artifact reserved field is non-zero",
464            ));
465        }
466        let artifact_id = input.u64()?;
467        let generation = input.u64()?;
468        let trained_vectors = input.u64()?;
469        let dimension = input.u32()?;
470        let tree_levels = input.u8()?;
471        let encoding_tag = input.u8()?;
472        let dimensions_per_block = input.u16()?;
473        let bits_per_code = input.u8()?;
474        let reserved = input.take(3)?;
475        if reserved != [0, 0, 0] {
476            return Err(ScannFormatError::new(
477                "ScaNN global artifact reserved bytes are non-zero",
478            ));
479        }
480        let config = ScannConfig {
481            dimension,
482            tree_levels,
483            num_leaves: input.u32()?,
484            encoding: ScannEncoding::from_parts(encoding_tag, dimensions_per_block, bits_per_code)?,
485        };
486        let level_count = input.u8()?;
487        if input.take(7)? != [0; 7] {
488            return Err(ScannFormatError::new(
489                "ScaNN global artifact level padding is non-zero",
490            ));
491        }
492        let mut levels = Vec::with_capacity(usize::from(level_count));
493        for _ in 0..level_count {
494            let centroid_count = input.u32()?;
495            let code_len = input.usize()?;
496            let minimum_count = input.usize()?;
497            let step_count = input.usize()?;
498            let child_count = input.usize()?;
499            let centroid_codes = input.take(code_len)?.to_vec();
500            let minimums = input.f32_vec(minimum_count)?;
501            let steps = input.f32_vec(step_count)?;
502            let child_offsets = input.u32_vec(child_count)?;
503            levels.push(ScannRoutingLevel {
504                centroid_count,
505                centroid_codes,
506                minimums,
507                steps,
508                child_offsets,
509            });
510        }
511        let ah_codebook = match input.u8()? {
512            0 => None,
513            1 => {
514                let dimensions_per_block = input.u16()?;
515                let centers_per_block = input.u16()?;
516                let center_count = input.usize()?;
517                Some(ScannAhCodebook {
518                    dimensions_per_block,
519                    centers_per_block,
520                    centers: input.f32_vec(center_count)?,
521                })
522            }
523            _ => {
524                return Err(ScannFormatError::new(
525                    "invalid ScaNN AH codebook presence tag",
526                ));
527            }
528        };
529        if !input.is_empty() {
530            return Err(ScannFormatError::new(
531                "ScaNN global artifact has trailing bytes",
532            ));
533        }
534        let artifact = Self {
535            generation,
536            artifact_id,
537            trained_vectors,
538            config,
539            levels,
540            ah_codebook,
541        };
542        artifact.validate()?;
543        Ok(artifact)
544    }
545
546    pub fn validate(&self) -> ScannResult<()> {
547        self.validate_shape()?;
548        if self.artifact_id == 0 {
549            return Err(ScannFormatError::new(
550                "ScaNN global artifact fingerprint must be non-zero",
551            ));
552        }
553        let expected = self.compute_fingerprint()?;
554        if self.artifact_id != expected {
555            return Err(ScannFormatError::new(
556                "ScaNN global artifact fingerprint mismatch",
557            ));
558        }
559        Ok(())
560    }
561
562    fn validate_shape(&self) -> ScannResult<()> {
563        self.config.validate()?;
564        if self.generation == 0 {
565            return Err(ScannFormatError::new(
566                "ScaNN global artifact generation must be non-zero",
567            ));
568        }
569        let required = self.config.effective_training_threshold()?;
570        if self.trained_vectors < required {
571            return Err(ScannFormatError::new(format!(
572                "ScaNN artifact was trained on {} vectors, below required threshold {required}",
573                self.trained_vectors
574            )));
575        }
576        if self.levels.len() != usize::from(self.config.tree_levels)
577            || self.levels.len() > usize::from(MAX_SCANN_TREE_LEVELS)
578        {
579            return Err(ScannFormatError::new(
580                "ScaNN routing level count does not match configuration",
581            ));
582        }
583        let centroid_width = match self.config.encoding {
584            ScannEncoding::AsymmetricHash { .. } => self.config.dimension as usize,
585            ScannEncoding::BinaryHamming => self.config.dimension as usize / 8,
586        };
587        for (level_index, level) in self.levels.iter().enumerate() {
588            let expected_centroid_bytes = (level.centroid_count as usize)
589                .checked_mul(centroid_width)
590                .ok_or_else(|| ScannFormatError::new("ScaNN centroid matrix size overflows"))?;
591            if level.centroid_count == 0 || level.centroid_codes.len() != expected_centroid_bytes {
592                return Err(ScannFormatError::new(format!(
593                    "invalid ScaNN centroid matrix at level {level_index}"
594                )));
595            }
596            match self.config.encoding {
597                ScannEncoding::AsymmetricHash { .. }
598                    if level.minimums.len() != self.config.dimension as usize
599                        || level.steps.len() != self.config.dimension as usize
600                        || level
601                            .minimums
602                            .iter()
603                            .chain(&level.steps)
604                            .any(|value| !value.is_finite()) =>
605                {
606                    return Err(ScannFormatError::new(format!(
607                        "invalid ScaNN fixed-point parameters at level {level_index}"
608                    )));
609                }
610                ScannEncoding::BinaryHamming
611                    if !level.minimums.is_empty() || !level.steps.is_empty() =>
612                {
613                    return Err(ScannFormatError::new(
614                        "binary ScaNN centroids must not carry float quantization parameters",
615                    ));
616                }
617                _ => {}
618            }
619            let is_leaf = level_index + 1 == self.levels.len();
620            if is_leaf {
621                if !level.child_offsets.is_empty() || level.centroid_count != self.config.num_leaves
622                {
623                    return Err(ScannFormatError::new(
624                        "ScaNN leaf level does not match configured leaves",
625                    ));
626                }
627            } else {
628                let next_count = self.levels[level_index + 1].centroid_count;
629                if level.child_offsets.len() != level.centroid_count as usize + 1
630                    || level.child_offsets.first() != Some(&0)
631                    || level.child_offsets.last() != Some(&next_count)
632                    || level.child_offsets.windows(2).any(|pair| pair[0] > pair[1])
633                {
634                    return Err(ScannFormatError::new(format!(
635                        "invalid ScaNN child directory at level {level_index}"
636                    )));
637                }
638            }
639        }
640        match (self.config.encoding, &self.ah_codebook) {
641            (
642                ScannEncoding::AsymmetricHash {
643                    dimensions_per_block,
644                    bits_per_code,
645                },
646                Some(codebook),
647            ) => {
648                let expected_centers = 1usize << bits_per_code;
649                let blocks =
650                    (self.config.dimension as usize).div_ceil(usize::from(dimensions_per_block));
651                let expected_values = blocks
652                    .checked_mul(expected_centers)
653                    .and_then(|count| count.checked_mul(usize::from(dimensions_per_block)))
654                    .ok_or_else(|| ScannFormatError::new("ScaNN AH codebook size overflows"))?;
655                if codebook.dimensions_per_block != dimensions_per_block
656                    || usize::from(codebook.centers_per_block) != expected_centers
657                    || codebook.centers.len() != expected_values
658                    || codebook.centers.iter().any(|value| !value.is_finite())
659                {
660                    return Err(ScannFormatError::new("invalid ScaNN AH codebook shape"));
661                }
662            }
663            (ScannEncoding::BinaryHamming, None) => {}
664            (ScannEncoding::AsymmetricHash { .. }, None) => {
665                return Err(ScannFormatError::new(
666                    "float ScaNN artifact is missing its AH codebook",
667                ));
668            }
669            (ScannEncoding::BinaryHamming, Some(_)) => {
670                return Err(ScannFormatError::new(
671                    "binary ScaNN must keep exact codes and has no AH codebook",
672                ));
673            }
674        }
675        Ok(())
676    }
677
678    fn encode(&self, stored_fingerprint: u64) -> ScannResult<Vec<u8>> {
679        let mut output = Vec::new();
680        self.for_each_encoded_chunk(stored_fingerprint, |chunk| {
681            output.extend_from_slice(chunk);
682            Ok(())
683        })?;
684        Ok(output)
685    }
686
687    fn compute_fingerprint(&self) -> ScannResult<u64> {
688        let mut hash = 0xcbf2_9ce4_8422_2325u64;
689        self.for_each_encoded_chunk(0, |chunk| {
690            for &byte in chunk {
691                hash ^= u64::from(byte);
692                hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
693            }
694            Ok(())
695        })?;
696        Ok(hash.max(1))
697    }
698
699    fn for_each_encoded_chunk(
700        &self,
701        stored_fingerprint: u64,
702        mut output: impl FnMut(&[u8]) -> ScannResult<()>,
703    ) -> ScannResult<()> {
704        output(MAGIC)?;
705        output(&SCANN_GLOBAL_ARTIFACT_VERSION.to_le_bytes())?;
706        output(&0u16.to_le_bytes())?;
707        output(&stored_fingerprint.to_le_bytes())?;
708        output(&self.generation.to_le_bytes())?;
709        output(&self.trained_vectors.to_le_bytes())?;
710        output(&self.config.dimension.to_le_bytes())?;
711        output(&[self.config.tree_levels])?;
712        output(&[self.config.encoding.tag()])?;
713        let (dimensions_per_block, bits_per_code) = self.config.encoding.parameters();
714        output(&dimensions_per_block.to_le_bytes())?;
715        output(&[bits_per_code])?;
716        output(&[0; 3])?;
717        output(&self.config.num_leaves.to_le_bytes())?;
718        output(&[u8::try_from(self.levels.len())
719            .map_err(|_| ScannFormatError::new("too many ScaNN routing levels"))?])?;
720        output(&[0; 7])?;
721        for level in &self.levels {
722            output(&level.centroid_count.to_le_bytes())?;
723            output(&encoded_len(level.centroid_codes.len())?)?;
724            output(&encoded_len(level.minimums.len())?)?;
725            output(&encoded_len(level.steps.len())?)?;
726            output(&encoded_len(level.child_offsets.len())?)?;
727            output(&level.centroid_codes)?;
728            for value in &level.minimums {
729                output(&value.to_bits().to_le_bytes())?;
730            }
731            for value in &level.steps {
732                output(&value.to_bits().to_le_bytes())?;
733            }
734            for &value in &level.child_offsets {
735                output(&value.to_le_bytes())?;
736            }
737        }
738        match &self.ah_codebook {
739            None => output(&[0])?,
740            Some(codebook) => {
741                output(&[1])?;
742                output(&codebook.dimensions_per_block.to_le_bytes())?;
743                output(&codebook.centers_per_block.to_le_bytes())?;
744                output(&encoded_len(codebook.centers.len())?)?;
745                for value in &codebook.centers {
746                    output(&value.to_bits().to_le_bytes())?;
747                }
748            }
749        }
750        Ok(())
751    }
752}
753
754fn fingerprint(bytes: &[u8]) -> u64 {
755    let mut hash = 0xcbf2_9ce4_8422_2325u64;
756    for (index, &byte) in bytes.iter().enumerate() {
757        let byte = if (FINGERPRINT_OFFSET..FINGERPRINT_OFFSET + 8).contains(&index) {
758            0
759        } else {
760            byte
761        };
762        hash ^= u64::from(byte);
763        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
764    }
765    hash.max(1)
766}
767
768fn encoded_len(value: usize) -> ScannResult<[u8; 8]> {
769    Ok(u64::try_from(value)
770        .map_err(|_| ScannFormatError::new("ScaNN artifact length exceeds u64"))?
771        .to_le_bytes())
772}
773
774struct Input<'a> {
775    bytes: &'a [u8],
776    offset: usize,
777}
778
779impl<'a> Input<'a> {
780    fn new(bytes: &'a [u8]) -> Self {
781        Self { bytes, offset: 0 }
782    }
783
784    fn take(&mut self, len: usize) -> ScannResult<&'a [u8]> {
785        let end = self
786            .offset
787            .checked_add(len)
788            .ok_or_else(|| ScannFormatError::new("ScaNN artifact offset overflows"))?;
789        let value = self
790            .bytes
791            .get(self.offset..end)
792            .ok_or_else(|| ScannFormatError::new("truncated ScaNN global artifact"))?;
793        self.offset = end;
794        Ok(value)
795    }
796
797    fn take_range(&mut self, len: usize) -> ScannResult<Range<usize>> {
798        let start = self.offset;
799        self.take(len)?;
800        Ok(start..self.offset)
801    }
802
803    fn u16(&mut self) -> ScannResult<u16> {
804        Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
805    }
806
807    fn u8(&mut self) -> ScannResult<u8> {
808        Ok(self.take(1)?[0])
809    }
810
811    fn u32(&mut self) -> ScannResult<u32> {
812        Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
813    }
814
815    fn u64(&mut self) -> ScannResult<u64> {
816        Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
817    }
818
819    fn usize(&mut self) -> ScannResult<usize> {
820        usize::try_from(self.u64()?)
821            .map_err(|_| ScannFormatError::new("ScaNN artifact length exceeds usize"))
822    }
823
824    fn f32_vec(&mut self, count: usize) -> ScannResult<Vec<f32>> {
825        let byte_len = count
826            .checked_mul(4)
827            .ok_or_else(|| ScannFormatError::new("ScaNN f32 vector size overflows"))?;
828        let bytes = self.take(byte_len)?;
829        Ok(bytes
830            .chunks_exact(4)
831            .map(|chunk| f32::from_le_bytes(chunk.try_into().unwrap()))
832            .collect())
833    }
834
835    fn u32_vec(&mut self, count: usize) -> ScannResult<Vec<u32>> {
836        let byte_len = count
837            .checked_mul(4)
838            .ok_or_else(|| ScannFormatError::new("ScaNN u32 vector size overflows"))?;
839        let bytes = self.take(byte_len)?;
840        Ok(bytes
841            .chunks_exact(4)
842            .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap()))
843            .collect())
844    }
845
846    fn is_empty(&self) -> bool {
847        self.offset == self.bytes.len()
848    }
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    fn binary_artifact() -> ScannTrainedArtifact {
856        let config = ScannConfig {
857            dimension: 16,
858            tree_levels: 2,
859            num_leaves: 4,
860            encoding: ScannEncoding::BinaryHamming,
861        };
862        ScannTrainedArtifact::new(
863            7,
864            100_000,
865            config,
866            vec![
867                ScannRoutingLevel {
868                    centroid_count: 2,
869                    centroid_codes: vec![0, 0, 0xff, 0xff],
870                    minimums: Vec::new(),
871                    steps: Vec::new(),
872                    child_offsets: vec![0, 2, 4],
873                },
874                ScannRoutingLevel {
875                    centroid_count: 4,
876                    centroid_codes: vec![0, 0, 1, 1, 0xfe, 0xfe, 0xff, 0xff],
877                    minimums: Vec::new(),
878                    steps: Vec::new(),
879                    child_offsets: Vec::new(),
880                },
881            ],
882            None,
883        )
884        .unwrap()
885    }
886
887    #[test]
888    fn global_artifact_round_trip_preserves_generation_and_fingerprint() {
889        let artifact = binary_artifact();
890        let bytes = artifact.to_bytes().unwrap();
891        let decoded = ScannTrainedArtifact::from_bytes(&bytes).unwrap();
892        assert_eq!(decoded, artifact);
893        assert_ne!(artifact.artifact_id, 0);
894    }
895
896    #[test]
897    fn streaming_artifact_writer_matches_in_memory_encoding() {
898        let artifact = binary_artifact();
899        let expected = artifact.to_bytes().unwrap();
900        let mut streamed = Vec::new();
901        let written = artifact.write_to(&mut streamed).unwrap();
902        assert_eq!(written as usize, expected.len());
903        assert_eq!(streamed, expected);
904    }
905
906    #[test]
907    fn global_artifact_view_borrows_the_original_centroid_plane() {
908        let artifact = binary_artifact();
909        let bytes = artifact.to_bytes().unwrap();
910        let view = ScannTrainedArtifactView::parse(&bytes).unwrap();
911        let level = view.level(0).unwrap();
912        let original = bytes.as_ptr_range();
913        assert_eq!(view.bytes().as_ptr(), bytes.as_ptr());
914        assert!(level.centroid_codes.as_ptr() >= original.start);
915        assert!(level.centroid_codes.as_ptr() < original.end);
916        assert_eq!(level.child_offsets().collect::<Vec<_>>(), vec![0, 2, 4]);
917        assert!(view.ah_codebook().is_none());
918    }
919
920    #[test]
921    fn global_artifact_view_rejects_truncation_and_corruption() {
922        let artifact = binary_artifact();
923        let bytes = artifact.to_bytes().unwrap();
924        assert!(ScannTrainedArtifactView::parse(&bytes[..bytes.len() - 1]).is_err());
925
926        let mut corrupt = bytes;
927        let centroid_offset = {
928            let view = ScannTrainedArtifactView::parse(&corrupt).unwrap();
929            view.level(0).unwrap().centroid_codes.as_ptr() as usize - corrupt.as_ptr() as usize
930        };
931        corrupt[centroid_offset] ^= 1;
932        let error = ScannTrainedArtifactView::parse(&corrupt).unwrap_err();
933        assert!(error.to_string().contains("fingerprint mismatch"));
934    }
935
936    #[test]
937    fn global_artifact_rejects_a_future_format_version() {
938        let artifact = binary_artifact();
939        let mut bytes = artifact.to_bytes().unwrap();
940        bytes[8..10].copy_from_slice(&(SCANN_GLOBAL_ARTIFACT_VERSION + 1).to_le_bytes());
941        let error = ScannTrainedArtifact::from_bytes(&bytes).unwrap_err();
942        assert!(error.to_string().contains("unsupported"));
943    }
944
945    #[test]
946    fn global_artifact_rejects_content_changed_after_fingerprinting() {
947        let artifact = binary_artifact();
948        let mut bytes = artifact.to_bytes().unwrap();
949        let centroid_offset = {
950            let view = ScannTrainedArtifactView::parse(&bytes).unwrap();
951            view.level(0).unwrap().centroid_codes.as_ptr() as usize - bytes.as_ptr() as usize
952        };
953        bytes[centroid_offset] ^= 1;
954        let error = ScannTrainedArtifact::from_bytes(&bytes).unwrap_err();
955        assert!(error.to_string().contains("fingerprint mismatch"));
956    }
957}