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#[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 pub child_offsets: Vec<u32>,
20}
21
22#[derive(Debug, Clone, PartialEq)]
25pub struct ScannAhCodebook {
26 pub dimensions_per_block: u16,
27 pub centers_per_block: u16,
28 pub centers: Vec<f32>,
30}
31
32#[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#[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 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 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}