1use core::ops::Range;
51use std::fs::File;
52use std::io::{self, BufWriter, Seek as _, Write};
53use std::path::Path;
54
55use memmap2::Mmap;
56
57use crate::error::{ArcsecError, Result};
58
59pub const MAGIC: &[u8; 8] = b"ARCSECIX";
61pub const VERSION: u32 = 1;
63pub const EXTENSION: &str = "arcsecix";
65
66const HEADER_LEN: usize = 256;
67const BYTE_ORDER: u32 = 0x0A0B_0C0D;
68const N_SECTIONS: usize = 5;
69const SECTIONS_AT: usize = 72;
70const HEADER_CRC_AT: usize = 252;
71const STAMP_AT: usize = 192;
72const TIER_LEN: usize = 40;
73const STAR_LEN: usize = 12;
74const QUAD_LEN: usize = 16;
75
76#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct TierInfo {
79 pub radius: f64,
82 pub mag_cap: f32,
84 pub members: u32,
86 pub first_pattern: u64,
88 pub n_patterns: u64,
90 pub n_anchors: u64,
92}
93
94impl TierInfo {
95 #[must_use]
97 pub fn patterns(&self) -> Range<usize> {
98 self.first_pattern as usize..(self.first_pattern + self.n_patterns) as usize
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct IndexStar {
105 pub ra: f32,
107 pub dec: f32,
109 pub mag: i16,
111 pub tier: u8,
113}
114
115#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
124pub struct SourceStamp {
125 pub files: u32,
127 pub bytes: u64,
129 pub hash: u64,
131}
132
133impl SourceStamp {
134 pub const HEAD_BYTES: usize = 4096;
136
137 #[must_use]
139 pub fn is_recorded(&self) -> bool {
140 self.files > 0
141 }
142
143 pub fn of_database(db_path: &Path, db_name: &str) -> Result<Self> {
150 use std::io::Read as _;
151 let prefix = format!("{db_name}_");
152 let mut names: Vec<String> = std::fs::read_dir(db_path)
153 .map_err(ArcsecError::CatalogIo)?
154 .filter_map(core::result::Result::ok)
155 .filter_map(|e| e.file_name().into_string().ok())
156 .filter(|n| {
157 n.starts_with(&prefix) && [".1476", ".290", ".001"].iter().any(|x| n.ends_with(x))
158 })
159 .collect();
160 names.sort();
161 let mut st = Self::default();
162 let mut h = Fnv::new();
163 let mut buf = vec![0u8; Self::HEAD_BYTES];
164 for n in &names {
165 let mut f = File::open(db_path.join(n)).map_err(ArcsecError::CatalogIo)?;
166 let len = f.metadata().map_err(ArcsecError::CatalogIo)?.len();
167 let mut got = 0;
168 while got < buf.len() {
169 match f.read(&mut buf[got..]) {
170 Ok(0) => break,
171 Ok(k) => got += k,
172 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
173 Err(e) => return Err(ArcsecError::CatalogIo(e)),
174 }
175 }
176 h.update(n.as_bytes());
177 h.update(&[0]);
178 h.update(&len.to_le_bytes());
179 h.update(&buf[..got]);
180 st.files += 1;
181 st.bytes += len;
182 }
183 st.hash = h.0;
184 Ok(st)
185 }
186}
187
188struct Fnv(u64);
191
192impl Fnv {
193 fn new() -> Self {
194 Self(0xCBF2_9CE4_8422_2325)
195 }
196 fn update(&mut self, bytes: &[u8]) {
197 for &b in bytes {
198 self.0 ^= u64::from(b);
199 self.0 = self.0.wrapping_mul(0x0100_0000_01B3);
200 }
201 }
202}
203
204#[derive(Debug, Default)]
206pub struct BuiltIndex {
207 pub tiers: Vec<TierInfo>,
209 pub stars: Vec<IndexStar>,
211 pub star_dir: Vec<u32>,
213 pub keys: Vec<u64>,
215 pub quads: Vec<[u32; 4]>,
217 pub source: String,
219 pub source_stamp: SourceStamp,
221}
222
223pub const STAR_BANDS: u32 = 720;
225
226#[must_use]
228pub fn star_band(dec: f64) -> u32 {
229 let b = ((dec + core::f64::consts::FRAC_PI_2) / core::f64::consts::PI * f64::from(STAR_BANDS))
230 .floor();
231 (b.max(0.0) as u32).min(STAR_BANDS - 1)
232}
233
234const fn crc_table() -> [u32; 256] {
237 let mut t = [0u32; 256];
238 let mut i = 0;
239 while i < 256 {
240 let mut c = i as u32;
241 let mut k = 0;
242 while k < 8 {
243 c = if c & 1 != 0 {
244 0xEDB8_8320 ^ (c >> 1)
245 } else {
246 c >> 1
247 };
248 k += 1;
249 }
250 t[i] = c;
251 i += 1;
252 }
253 t
254}
255
256static CRC_TABLE: [u32; 256] = crc_table();
257
258#[derive(Clone, Copy)]
260struct Crc(u32);
261
262impl Crc {
263 fn new() -> Self {
264 Self(0xFFFF_FFFF)
265 }
266 fn update(&mut self, bytes: &[u8]) {
267 let mut c = self.0;
268 for &b in bytes {
269 c = CRC_TABLE[((c ^ u32::from(b)) & 0xFF) as usize] ^ (c >> 8);
270 }
271 self.0 = c;
272 }
273 fn finish(self) -> u32 {
274 !self.0
275 }
276}
277
278fn crc32(bytes: &[u8]) -> u32 {
279 let mut c = Crc::new();
280 c.update(bytes);
281 c.finish()
282}
283
284struct SectionWriter<W: Write> {
288 w: W,
289 pos: u64,
290 crc: Crc,
291}
292
293impl<W: Write> SectionWriter<W> {
294 fn put(&mut self, b: &[u8]) -> io::Result<()> {
295 self.w.write_all(b)?;
296 self.crc.update(b);
297 self.pos += b.len() as u64;
298 Ok(())
299 }
300 fn begin(&mut self) -> io::Result<u64> {
302 let pad = (8 - self.pos % 8) % 8;
303 self.w.write_all(&[0u8; 8][..pad as usize])?;
304 self.pos += pad;
305 self.crc = Crc::new();
306 Ok(self.pos)
307 }
308}
309
310impl BuiltIndex {
311 #[must_use]
313 pub fn file_size(&self) -> u64 {
314 (HEADER_LEN
315 + self.tiers.len() * TIER_LEN
316 + self.stars.len() * STAR_LEN
317 + self.star_dir.len() * 4
318 + self.keys.len() * 8
319 + self.quads.len() * QUAD_LEN
320 + 32) as u64
321 }
322
323 pub fn write(&self, path: &Path) -> Result<()> {
330 let tmp = path.with_extension(format!("{EXTENSION}.part"));
331 self.write_to(&tmp).map_err(ArcsecError::CatalogIo)?;
332 std::fs::rename(&tmp, path).map_err(ArcsecError::CatalogIo)
333 }
334
335 fn write_to(&self, path: &Path) -> io::Result<()> {
336 let f = File::create(path)?;
337 let mut sw = SectionWriter {
338 w: BufWriter::with_capacity(1 << 20, f),
339 pos: 0,
340 crc: Crc::new(),
341 };
342 sw.put(&[0u8; HEADER_LEN])?;
344 let mut sections = [(0u64, 0u64, 0u32); N_SECTIONS];
345
346 let off = sw.begin()?;
347 for t in &self.tiers {
348 sw.put(&t.radius.to_le_bytes())?;
349 sw.put(&t.mag_cap.to_le_bytes())?;
350 sw.put(&t.members.to_le_bytes())?;
351 sw.put(&t.first_pattern.to_le_bytes())?;
352 sw.put(&t.n_patterns.to_le_bytes())?;
353 sw.put(&t.n_anchors.to_le_bytes())?;
354 }
355 sections[0] = (off, sw.pos - off, sw.crc.finish());
356
357 let off = sw.begin()?;
358 for s in &self.stars {
359 let mut r = [0u8; STAR_LEN];
360 r[0..4].copy_from_slice(&s.ra.to_le_bytes());
361 r[4..8].copy_from_slice(&s.dec.to_le_bytes());
362 r[8..10].copy_from_slice(&s.mag.to_le_bytes());
363 r[10] = s.tier;
364 sw.put(&r)?;
365 }
366 sections[1] = (off, sw.pos - off, sw.crc.finish());
367
368 let off = sw.begin()?;
369 for &d in &self.star_dir {
370 sw.put(&d.to_le_bytes())?;
371 }
372 sections[2] = (off, sw.pos - off, sw.crc.finish());
373
374 let off = sw.begin()?;
375 for &k in &self.keys {
376 sw.put(&k.to_le_bytes())?;
377 }
378 sections[3] = (off, sw.pos - off, sw.crc.finish());
379
380 let off = sw.begin()?;
381 for q in &self.quads {
382 let mut r = [0u8; QUAD_LEN];
383 for (i, &s) in q.iter().enumerate() {
384 r[i * 4..i * 4 + 4].copy_from_slice(&s.to_le_bytes());
385 }
386 sw.put(&r)?;
387 }
388 sections[4] = (off, sw.pos - off, sw.crc.finish());
389
390 let mut h = [0u8; HEADER_LEN];
391 h[0..8].copy_from_slice(MAGIC);
392 h[8..12].copy_from_slice(&VERSION.to_le_bytes());
393 h[12..16].copy_from_slice(&BYTE_ORDER.to_le_bytes());
394 h[16..20].copy_from_slice(&(HEADER_LEN as u32).to_le_bytes());
395 h[20..24].copy_from_slice(&(super::pattern::BINS as u32).to_le_bytes());
396 h[24..28].copy_from_slice(&(self.tiers.len() as u32).to_le_bytes());
397 h[28..32].copy_from_slice(&STAR_BANDS.to_le_bytes());
398 h[32..40].copy_from_slice(&(self.stars.len() as u64).to_le_bytes());
399 h[40..48].copy_from_slice(&(self.keys.len() as u64).to_le_bytes());
400 let now = std::time::SystemTime::now()
401 .duration_since(std::time::UNIX_EPOCH)
402 .map_or(0, |d| d.as_secs() as i64);
403 h[48..56].copy_from_slice(&now.to_le_bytes());
404 let src = self.source.as_bytes();
405 let n = src.len().min(16);
406 h[56..56 + n].copy_from_slice(&src[..n]);
407 h[STAMP_AT..STAMP_AT + 8].copy_from_slice(&self.source_stamp.hash.to_le_bytes());
408 h[STAMP_AT + 8..STAMP_AT + 16].copy_from_slice(&self.source_stamp.bytes.to_le_bytes());
409 h[STAMP_AT + 16..STAMP_AT + 20].copy_from_slice(&self.source_stamp.files.to_le_bytes());
410 for (i, (o, l, c)) in sections.iter().enumerate() {
411 let at = SECTIONS_AT + i * 24;
412 h[at..at + 8].copy_from_slice(&o.to_le_bytes());
413 h[at + 8..at + 16].copy_from_slice(&l.to_le_bytes());
414 h[at + 16..at + 20].copy_from_slice(&c.to_le_bytes());
415 }
416 let crc = crc32(&h[..HEADER_CRC_AT]);
417 h[HEADER_CRC_AT..].copy_from_slice(&crc.to_le_bytes());
418
419 let mut f = sw.w.into_inner().map_err(io::IntoInnerError::into_error)?;
420
421 f.seek(io::SeekFrom::Start(0))?;
422 f.write_all(&h)?;
423 f.sync_all()
424 }
425}
426
427#[inline]
430fn u32_at(b: &[u8], at: usize) -> u32 {
431 u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]])
432}
433
434#[inline]
435fn u64_at(b: &[u8], at: usize) -> u64 {
436 let mut x = [0u8; 8];
437 x.copy_from_slice(&b[at..at + 8]);
438 u64::from_le_bytes(x)
439}
440
441#[inline]
442fn f32_at(b: &[u8], at: usize) -> f32 {
443 f32::from_bits(u32_at(b, at))
444}
445
446fn invalid(path: &Path, why: impl core::fmt::Display) -> ArcsecError {
447 ArcsecError::CatalogIo(io::Error::new(
448 io::ErrorKind::InvalidData,
449 format!("{}: not a usable arcsec blind index: {why}", path.display()),
450 ))
451}
452
453pub struct BlindIndex {
455 map: Mmap,
456 tiers: Vec<TierInfo>,
457 sections: [(usize, usize, u32); N_SECTIONS],
458 n_stars: usize,
459 n_patterns: usize,
460 star_bands: u32,
461 source: String,
462 source_stamp: SourceStamp,
463 built_unix: i64,
464}
465
466impl core::fmt::Debug for BlindIndex {
467 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
468 f.debug_struct("BlindIndex")
469 .field("source", &self.source)
470 .field("n_stars", &self.n_stars)
471 .field("n_patterns", &self.n_patterns)
472 .field("tiers", &self.tiers)
473 .finish_non_exhaustive()
474 }
475}
476
477#[must_use]
479pub fn is_blind_index(path: &Path) -> bool {
480 use std::io::Read as _;
481 let mut m = [0u8; 8];
482 File::open(path)
483 .and_then(|mut f| f.read_exact(&mut m))
484 .is_ok()
485 && &m == MAGIC
486}
487
488impl BlindIndex {
489 pub fn open(path: &Path) -> Result<Self> {
497 let file = File::open(path).map_err(ArcsecError::CatalogIo)?;
498 let map = unsafe { Mmap::map(&file) }.map_err(ArcsecError::CatalogIo)?;
502 if map.len() < HEADER_LEN || &map[..8] != MAGIC {
503 return Err(invalid(path, "bad magic"));
504 }
505 let version = u32_at(&map, 8);
506 if version != VERSION {
507 return Err(invalid(
508 path,
509 format!("format version {version}, this build reads {VERSION} - rebuild it"),
510 ));
511 }
512 if u32_at(&map, 12) != BYTE_ORDER {
513 return Err(invalid(path, "written with the other byte order"));
514 }
515 if crc32(&map[..HEADER_CRC_AT]) != u32_at(&map, HEADER_CRC_AT) {
516 return Err(invalid(path, "header checksum mismatch"));
517 }
518 if u32_at(&map, 16) as usize != HEADER_LEN
519 || f64::from(u32_at(&map, 20)) != super::pattern::BINS
520 {
521 return Err(invalid(
522 path,
523 "unsupported header length or descriptor bins",
524 ));
525 }
526 let n_tiers = u32_at(&map, 24) as usize;
527 let star_bands = u32_at(&map, 28);
528 let n_stars = usize::try_from(u64_at(&map, 32)).map_err(|_| invalid(path, "too large"))?;
529 let n_patterns =
530 usize::try_from(u64_at(&map, 40)).map_err(|_| invalid(path, "too large"))?;
531 let built_unix = u64_at(&map, 48) as i64;
532 let source = String::from_utf8_lossy(&map[56..72])
533 .trim_end_matches('\0')
534 .to_string();
535 let source_stamp = SourceStamp {
536 hash: u64_at(&map, STAMP_AT),
537 bytes: u64_at(&map, STAMP_AT + 8),
538 files: u32_at(&map, STAMP_AT + 16),
539 };
540
541 let mut sections = [(0usize, 0usize, 0u32); N_SECTIONS];
542 let expected = [
543 n_tiers.checked_mul(TIER_LEN),
544 n_stars.checked_mul(STAR_LEN),
545 (star_bands as usize + 1).checked_mul(4),
546 n_patterns.checked_mul(8),
547 n_patterns.checked_mul(QUAD_LEN),
548 ];
549 for (i, s) in sections.iter_mut().enumerate() {
550 let at = SECTIONS_AT + i * 24;
551 let off = usize::try_from(u64_at(&map, at)).map_err(|_| invalid(path, "too large"))?;
552 let len =
553 usize::try_from(u64_at(&map, at + 8)).map_err(|_| invalid(path, "too large"))?;
554 if Some(len) != expected[i] || off.checked_add(len).is_none_or(|e| e > map.len()) {
555 return Err(invalid(
556 path,
557 format!("section {i} is truncated or mis-sized"),
558 ));
559 }
560 *s = (off, len, u32_at(&map, at + 16));
561 }
562
563 let tb = &map[sections[0].0..sections[0].0 + sections[0].1];
564 let mut tiers = Vec::with_capacity(n_tiers);
565 for t in 0..n_tiers {
566 let r = &tb[t * TIER_LEN..(t + 1) * TIER_LEN];
567 let tier = TierInfo {
568 radius: f64::from_bits(u64_at(r, 0)),
569 mag_cap: f32_at(r, 8),
570 members: u32_at(r, 12),
571 first_pattern: u64_at(r, 16),
572 n_patterns: u64_at(r, 24),
573 n_anchors: u64_at(r, 32),
574 };
575 let end = tier.first_pattern.checked_add(tier.n_patterns);
576 if !(tier.radius.is_finite() && tier.radius > 0.0)
577 || end.is_none_or(|e| e > n_patterns as u64)
578 {
579 return Err(invalid(path, format!("tier {t} is inconsistent")));
580 }
581 tiers.push(tier);
582 }
583
584 Ok(Self {
585 map,
586 tiers,
587 sections,
588 n_stars,
589 n_patterns,
590 star_bands,
591 source,
592 source_stamp,
593 built_unix,
594 })
595 }
596
597 pub fn validate(&self) -> Result<()> {
603 const NAMES: [&str; N_SECTIONS] = ["tiers", "stars", "star directory", "keys", "quads"];
604 for (i, &(off, len, crc)) in self.sections.iter().enumerate() {
605 if crc32(&self.map[off..off + len]) != crc {
606 return Err(ArcsecError::CatalogIo(io::Error::new(
607 io::ErrorKind::InvalidData,
608 format!("blind index: {} section checksum mismatch", NAMES[i]),
609 )));
610 }
611 }
612 Ok(())
613 }
614
615 #[must_use]
617 pub fn tiers(&self) -> &[TierInfo] {
618 &self.tiers
619 }
620
621 #[must_use]
623 pub fn n_stars(&self) -> usize {
624 self.n_stars
625 }
626
627 #[must_use]
629 pub fn n_patterns(&self) -> usize {
630 self.n_patterns
631 }
632
633 #[must_use]
635 pub fn source(&self) -> &str {
636 &self.source
637 }
638
639 #[must_use]
642 pub fn source_stamp(&self) -> SourceStamp {
643 self.source_stamp
644 }
645
646 #[must_use]
648 pub fn built_unix(&self) -> i64 {
649 self.built_unix
650 }
651
652 #[must_use]
654 pub fn file_size(&self) -> usize {
655 self.map.len()
656 }
657
658 #[must_use]
660 pub fn tier_bytes(&self, t: &TierInfo) -> u64 {
661 t.n_patterns * (8 + QUAD_LEN as u64)
662 }
663
664 #[inline]
665 fn key(&self, i: usize) -> u64 {
666 u64_at(&self.map, self.sections[3].0 + i * 8)
667 }
668
669 #[must_use]
671 pub fn lookup(&self, tier: &TierInfo, key: u64) -> Range<usize> {
672 let r = tier.patterns();
673 let (mut lo, mut hi) = (r.start, r.end);
674 while lo < hi {
675 let mid = lo + (hi - lo) / 2;
676 if self.key(mid) < key {
677 lo = mid + 1;
678 } else {
679 hi = mid;
680 }
681 }
682 let start = lo;
683 let mut hi = r.end;
684 while lo < hi {
685 let mid = lo + (hi - lo) / 2;
686 if self.key(mid) <= key {
687 lo = mid + 1;
688 } else {
689 hi = mid;
690 }
691 }
692 start..lo
693 }
694
695 #[must_use]
698 pub fn quad(&self, i: usize) -> Option<[IndexStar; 4]> {
699 if i >= self.n_patterns {
700 return None;
701 }
702 let at = self.sections[4].0 + i * QUAD_LEN;
703 let mut out = [IndexStar {
704 ra: 0.0,
705 dec: 0.0,
706 mag: 0,
707 tier: 0,
708 }; 4];
709 for (k, s) in out.iter_mut().enumerate() {
710 *s = self.star(u32_at(&self.map, at + k * 4) as usize)?;
711 }
712 Some(out)
713 }
714
715 #[must_use]
717 pub fn star(&self, i: usize) -> Option<IndexStar> {
718 if i >= self.n_stars {
719 return None;
720 }
721 let at = self.sections[1].0 + i * STAR_LEN;
722 let b = &self.map[at..at + STAR_LEN];
723 Some(IndexStar {
724 ra: f32_at(b, 0),
725 dec: f32_at(b, 4),
726 mag: i16::from_le_bytes([b[8], b[9]]),
727 tier: b[10],
728 })
729 }
730
731 pub fn stars_near(&self, ra: f64, dec: f64, radius: f64, mut f: impl FnMut(&IndexStar)) {
735 use core::f64::consts::{FRAC_PI_2, PI};
736 if self.star_bands != STAR_BANDS {
737 return;
738 }
739 let lo_b = star_band((dec - radius).max(-FRAC_PI_2));
740 let hi_b = star_band((dec + radius).min(FRAC_PI_2));
741 let dir_at = self.sections[2].0;
742 for band in lo_b..=hi_b {
743 let s0 = u32_at(&self.map, dir_at + band as usize * 4) as usize;
744 let s1 =
745 (u32_at(&self.map, dir_at + (band as usize + 1) * 4) as usize).min(self.n_stars);
746 if s0 >= s1 {
747 continue;
748 }
749 let b_lo = f64::from(band) / f64::from(STAR_BANDS) * PI - FRAC_PI_2;
751 let b_hi = b_lo + PI / f64::from(STAR_BANDS);
752 let cos_min = b_lo.cos().min(b_hi.cos()).max(0.0);
753 let half = if cos_min * PI <= radius || dec.abs() + radius >= FRAC_PI_2 {
754 PI
755 } else {
756 (radius / cos_min).min(PI)
757 };
758 if half >= PI {
759 for i in s0..s1 {
760 if let Some(s) = self.star(i) {
761 f(&s);
762 }
763 }
764 continue;
765 }
766 let ra0 = (ra - half).rem_euclid(2.0 * PI);
767 let ra1 = (ra + half).rem_euclid(2.0 * PI);
768 let windows: &[(f64, f64)] = if ra0 <= ra1 {
769 &[(ra0, ra1)]
770 } else {
771 &[(ra0, 2.0 * PI), (0.0, ra1)]
772 };
773 for &(w0, w1) in windows {
774 let (mut lo, mut hi) = (s0, s1);
776 while lo < hi {
777 let mid = lo + (hi - lo) / 2;
778 let r = self.star(mid).map_or(f32::INFINITY, |s| s.ra);
779 if f64::from(r) < w0 {
780 lo = mid + 1;
781 } else {
782 hi = mid;
783 }
784 }
785 for i in lo..s1 {
786 let Some(s) = self.star(i) else { break };
787 if f64::from(s.ra) > w1 {
788 break;
789 }
790 f(&s);
791 }
792 }
793 }
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use crate::test_support::TempDir;
801
802 fn sample() -> BuiltIndex {
803 let stars: Vec<IndexStar> = (0..10)
804 .map(|i| IndexStar {
805 ra: 0.1 * i as f32,
806 dec: 0.2,
807 mag: 1000 + i,
808 tier: 0,
809 })
810 .collect();
811 let mut dir = vec![0u32; STAR_BANDS as usize + 1];
812 let b = star_band(0.2) as usize;
813 for (i, d) in dir.iter_mut().enumerate() {
814 *d = if i <= b { 0 } else { 10 };
815 }
816 BuiltIndex {
817 tiers: vec![TierInfo {
818 radius: 0.01,
819 mag_cap: 12.0,
820 members: 5,
821 first_pattern: 0,
822 n_patterns: 3,
823 n_anchors: 1,
824 }],
825 stars,
826 star_dir: dir,
827 keys: vec![5, 7, 7],
828 quads: vec![[0, 1, 2, 3], [1, 2, 3, 4], [5, 6, 7, 8]],
829 source: "d80".into(),
830 source_stamp: SourceStamp::default(),
831 }
832 }
833
834 #[test]
835 fn round_trips_and_looks_up() {
836 let dir = TempDir::new("arcsecix_rt");
837 let p = dir.path().join("t.arcsecix");
838 sample().write(&p).unwrap();
839 assert!(is_blind_index(&p));
840 let ix = BlindIndex::open(&p).unwrap();
841 ix.validate().unwrap();
842 assert_eq!(ix.source(), "d80");
843 assert_eq!(ix.n_stars(), 10);
844 let t = ix.tiers()[0];
845 assert_eq!(ix.lookup(&t, 7), 1..3);
846 assert_eq!(ix.lookup(&t, 5), 0..1);
847 assert!(ix.lookup(&t, 6).is_empty());
848 assert_eq!(ix.quad(2).unwrap()[3].mag, 1008);
849 let mut n = 0;
850 ix.stars_near(0.3, 0.2, 0.11, |_| n += 1);
851 assert!((3..=5).contains(&n), "{n}");
852 }
853
854 #[test]
855 fn rejects_bad_magic_truncation_and_corruption() {
856 let dir = TempDir::new("arcsecix_bad");
857 let p = dir.path().join("t.arcsecix");
858 sample().write(&p).unwrap();
859 let good = std::fs::read(&p).unwrap();
860
861 let mut b = good.clone();
862 b[0] = b'X';
863 std::fs::write(&p, &b).unwrap();
864 assert!(BlindIndex::open(&p).is_err());
865
866 std::fs::write(&p, &good[..good.len() - 20]).unwrap();
867 assert!(BlindIndex::open(&p).is_err(), "truncated");
868
869 let mut b = good.clone();
870 b[30] ^= 1; std::fs::write(&p, &b).unwrap();
872 assert!(BlindIndex::open(&p).is_err());
873
874 let mut b = good.clone();
875 let n = b.len();
876 b[n - 3] ^= 0x40; std::fs::write(&p, &b).unwrap();
878 let ix = BlindIndex::open(&p).unwrap();
879 assert!(ix.validate().is_err());
880 }
881
882 #[test]
883 fn a_corrupt_star_reference_is_refused_not_followed() {
884 let dir = TempDir::new("arcsecix_ref");
885 let p = dir.path().join("t.arcsecix");
886 let mut s = sample();
887 s.quads[0] = [0, 1, 2, 999];
888 s.write(&p).unwrap();
889 let ix = BlindIndex::open(&p).unwrap();
890 assert!(ix.quad(0).is_none());
891 assert!(ix.quad(99).is_none());
892 }
893
894 #[test]
895 fn the_source_stamp_round_trips_and_an_unstamped_file_reads_as_unrecorded() {
896 let dir = TempDir::new("arcsecix_stamp");
897 let p = dir.path().join("t.arcsecix");
898 sample().write(&p).unwrap();
899 let ix = BlindIndex::open(&p).unwrap();
900 assert!(!ix.source_stamp().is_recorded(), "zeros mean not recorded");
901
902 let mut s = sample();
903 s.source_stamp = SourceStamp {
904 files: 1476,
905 bytes: 1_300_000_000,
906 hash: 0x0123_4567_89AB_CDEF,
907 };
908 s.write(&p).unwrap();
909 let ix = BlindIndex::open(&p).unwrap();
910 ix.validate().unwrap();
911 assert_eq!(ix.source_stamp(), s.source_stamp);
912 }
913
914 #[test]
915 fn the_database_stamp_sees_size_and_content_but_not_other_files() {
916 let dir = TempDir::new("arcsecix_dbstamp");
917 let d = dir.path();
918 std::fs::write(d.join("t_0101.1476"), vec![1u8; 5000]).unwrap();
919 std::fs::write(d.join("t_0201.1476"), vec![2u8; 300]).unwrap();
920 std::fs::write(d.join("u_0101.1476"), b"another database").unwrap();
921 std::fs::write(d.join("t.arcsecix"), b"not a database file").unwrap();
922 let a = SourceStamp::of_database(d, "t").unwrap();
923 assert_eq!((a.files, a.bytes), (2, 5300));
924 assert!(a.is_recorded());
925 assert_eq!(
926 a,
927 SourceStamp::of_database(d, "t").unwrap(),
928 "deterministic"
929 );
930
931 std::fs::write(d.join("u_0201.1476"), b"more").unwrap();
933 assert_eq!(a, SourceStamp::of_database(d, "t").unwrap());
934
935 let mut b = vec![1u8; 5000];
937 b[10] = 9;
938 std::fs::write(d.join("t_0101.1476"), &b).unwrap();
939 let c = SourceStamp::of_database(d, "t").unwrap();
940 assert_eq!(c.bytes, a.bytes);
941 assert_ne!(c.hash, a.hash);
942
943 assert!(!SourceStamp::of_database(d, "zz").unwrap().is_recorded());
945 }
946
947 #[test]
948 fn crc_matches_the_standard_check_value() {
949 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
950 }
951}