Skip to main content

supercov_engine/
query_index.rs

1//! Immutable, versioned, fixed-layout query-index container.
2//!
3//! The evidence archive remains authoritative. This index is disposable and
4//! bound to the exact evidence, analysis inputs and producer ABI. Sections are
5//! opaque to this container; higher layers define their typed record layouts.
6//! Reads validate the authenticated header, all checked layout invariants and
7//! the SHA-256 digest of every data page they touch.
8
9use std::{
10    collections::{BTreeSet, HashSet},
11    fs::{self, File, OpenOptions},
12    io::{self, Seek, SeekFrom, Write},
13    path::{Path, PathBuf},
14    sync::{
15        Mutex,
16        atomic::{AtomicU64, Ordering},
17    },
18};
19
20use memmap2::Mmap;
21use sha2::{Digest, Sha256};
22
23pub const QUERY_INDEX_SCHEMA_VERSION: u32 = 4;
24pub const QUERY_INDEX_HEADER_SIZE: usize = 4_096;
25pub const QUERY_INDEX_PAGE_SIZE: usize = 64 * 1_024;
26pub const QUERY_INDEX_MAX_SECTIONS: usize = 48;
27const MAGIC: &[u8; 8] = b"SCQIDX01";
28const HEADER_CHECKSUM_OFFSET: usize = QUERY_INDEX_HEADER_SIZE - 32;
29const DESCRIPTOR_OFFSET: usize = 256;
30const DESCRIPTOR_SIZE: usize = 64;
31static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct QueryIndexIdentity {
35    pub evidence_sha256: [u8; 32],
36    pub evidence_bytes: u64,
37    pub analysis_sha256: [u8; 32],
38    pub producer_sha256: [u8; 32],
39    pub archive_schema_version: u32,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct QueryIndexSection {
44    pub kind: u32,
45    pub record_size: u32,
46    pub count: u64,
47    pub bytes: Vec<u8>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct SectionDescriptor {
52    pub kind: u32,
53    pub record_size: u32,
54    pub offset: u64,
55    pub length: u64,
56    pub count: u64,
57    pub digests_offset: u64,
58    pub digest_count: u32,
59}
60
61#[derive(Debug)]
62pub enum QueryIndexError {
63    Io(io::Error),
64    NotRegularFile(PathBuf),
65    InvalidHeader(&'static str),
66    IdentityMismatch(&'static str),
67    TooManySections(usize),
68    InvalidSection { kind: u32, reason: &'static str },
69    DuplicateSection(u32),
70    MissingSection(u32),
71    OutOfBounds { kind: u32, offset: u64, length: u64 },
72    CorruptPage { kind: u32, page: usize },
73    SizeOverflow,
74}
75
76impl std::fmt::Display for QueryIndexError {
77    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::Io(error) => write!(formatter, "{error}"),
80            Self::NotRegularFile(path) => write!(
81                formatter,
82                "query index is not a regular file: {}",
83                path.display()
84            ),
85            Self::InvalidHeader(reason) => {
86                write!(formatter, "invalid query-index header: {reason}")
87            }
88            Self::IdentityMismatch(field) => write!(formatter, "stale query-index {field}"),
89            Self::TooManySections(count) => write!(formatter, "query index has {count} sections"),
90            Self::InvalidSection { kind, reason } => {
91                write!(formatter, "invalid query-index section {kind}: {reason}")
92            }
93            Self::DuplicateSection(kind) => {
94                write!(formatter, "duplicate query-index section {kind}")
95            }
96            Self::MissingSection(kind) => write!(formatter, "missing query-index section {kind}"),
97            Self::OutOfBounds {
98                kind,
99                offset,
100                length,
101            } => write!(
102                formatter,
103                "query-index section {kind} range {offset}+{length} is out of bounds"
104            ),
105            Self::CorruptPage { kind, page } => {
106                write!(
107                    formatter,
108                    "query-index section {kind} page {page} is corrupt"
109                )
110            }
111            Self::SizeOverflow => write!(formatter, "query index exceeds its format limits"),
112        }
113    }
114}
115
116impl std::error::Error for QueryIndexError {}
117
118impl From<io::Error> for QueryIndexError {
119    fn from(error: io::Error) -> Self {
120        Self::Io(error)
121    }
122}
123
124fn get<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N], QueryIndexError> {
125    bytes
126        .get(offset..offset + N)
127        .and_then(|slice| slice.try_into().ok())
128        .ok_or(QueryIndexError::InvalidHeader("truncated field"))
129}
130
131fn get_u32(bytes: &[u8], offset: usize) -> Result<u32, QueryIndexError> {
132    Ok(u32::from_le_bytes(get(bytes, offset)?))
133}
134
135fn get_u64(bytes: &[u8], offset: usize) -> Result<u64, QueryIndexError> {
136    Ok(u64::from_le_bytes(get(bytes, offset)?))
137}
138
139fn put_u32(bytes: &mut [u8], offset: usize, value: u32) {
140    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
141}
142
143fn put_u64(bytes: &mut [u8], offset: usize, value: u64) {
144    bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
145}
146
147fn checked_end(offset: u64, length: u64) -> Result<u64, QueryIndexError> {
148    offset
149        .checked_add(length)
150        .ok_or(QueryIndexError::SizeOverflow)
151}
152
153fn align(value: u64, alignment: u64) -> Result<u64, QueryIndexError> {
154    value
155        .checked_add(alignment - 1)
156        .map(|value| value / alignment * alignment)
157        .ok_or(QueryIndexError::SizeOverflow)
158}
159
160fn temporary_path(destination: &Path, sequence: u64) -> Result<PathBuf, QueryIndexError> {
161    let name = destination
162        .file_name()
163        .and_then(|name| name.to_str())
164        .ok_or(QueryIndexError::InvalidHeader("invalid destination name"))?;
165    Ok(destination.with_file_name(format!(".{name}.{}.{}.tmp", std::process::id(), sequence)))
166}
167
168fn layout_sections(
169    sections: &[QueryIndexSection],
170) -> Result<(Vec<SectionDescriptor>, u64), QueryIndexError> {
171    if sections.len() > QUERY_INDEX_MAX_SECTIONS {
172        return Err(QueryIndexError::TooManySections(sections.len()));
173    }
174    let mut seen = HashSet::new();
175    let mut ordered = sections.iter().collect::<Vec<_>>();
176    ordered.sort_by_key(|section| section.kind);
177    for section in &ordered {
178        if section.kind == 0 {
179            return Err(QueryIndexError::InvalidSection {
180                kind: 0,
181                reason: "kind zero is reserved",
182            });
183        }
184        if !seen.insert(section.kind) {
185            return Err(QueryIndexError::DuplicateSection(section.kind));
186        }
187        if section.record_size > 0
188            && u64::from(section.record_size)
189                .checked_mul(section.count)
190                .ok_or(QueryIndexError::SizeOverflow)?
191                != section.bytes.len() as u64
192        {
193            return Err(QueryIndexError::InvalidSection {
194                kind: section.kind,
195                reason: "record size times count does not equal section length",
196            });
197        }
198    }
199    let mut cursor = QUERY_INDEX_HEADER_SIZE as u64;
200    let mut descriptors = Vec::with_capacity(ordered.len());
201    for section in &ordered {
202        cursor = align(cursor, 8)?;
203        let length =
204            u64::try_from(section.bytes.len()).map_err(|_| QueryIndexError::SizeOverflow)?;
205        let offset = cursor;
206        cursor = checked_end(cursor, length)?;
207        descriptors.push(SectionDescriptor {
208            kind: section.kind,
209            record_size: section.record_size,
210            offset,
211            length,
212            count: section.count,
213            digests_offset: 0,
214            digest_count: u32::try_from(section.bytes.len().div_ceil(QUERY_INDEX_PAGE_SIZE))
215                .map_err(|_| QueryIndexError::SizeOverflow)?,
216        });
217    }
218    for descriptor in &mut descriptors {
219        cursor = align(cursor, 8)?;
220        descriptor.digests_offset = cursor;
221        cursor = checked_end(cursor, u64::from(descriptor.digest_count) * 32)?;
222    }
223    Ok((descriptors, cursor))
224}
225
226fn make_header(
227    identity: &QueryIndexIdentity,
228    descriptors: &[SectionDescriptor],
229    total_bytes: u64,
230) -> Vec<u8> {
231    let mut header = vec![0_u8; QUERY_INDEX_HEADER_SIZE];
232    header[..8].copy_from_slice(MAGIC);
233    put_u32(&mut header, 8, QUERY_INDEX_SCHEMA_VERSION);
234    put_u32(&mut header, 12, QUERY_INDEX_HEADER_SIZE as u32);
235    put_u64(&mut header, 16, total_bytes);
236    put_u64(&mut header, 24, identity.evidence_bytes);
237    put_u32(&mut header, 32, identity.archive_schema_version);
238    put_u32(&mut header, 36, descriptors.len() as u32);
239    put_u32(&mut header, 40, QUERY_INDEX_PAGE_SIZE as u32);
240    header[48..80].copy_from_slice(&identity.evidence_sha256);
241    header[80..112].copy_from_slice(&identity.analysis_sha256);
242    header[112..144].copy_from_slice(&identity.producer_sha256);
243    for (index, descriptor) in descriptors.iter().enumerate() {
244        let offset = DESCRIPTOR_OFFSET + index * DESCRIPTOR_SIZE;
245        put_u32(&mut header, offset, descriptor.kind);
246        put_u32(&mut header, offset + 4, descriptor.record_size);
247        put_u64(&mut header, offset + 8, descriptor.offset);
248        put_u64(&mut header, offset + 16, descriptor.length);
249        put_u64(&mut header, offset + 24, descriptor.count);
250        put_u64(&mut header, offset + 32, descriptor.digests_offset);
251        put_u32(&mut header, offset + 40, descriptor.digest_count);
252    }
253    let checksum = Sha256::digest(&header[..HEADER_CHECKSUM_OFFSET]);
254    header[HEADER_CHECKSUM_OFFSET..].copy_from_slice(&checksum);
255    header
256}
257
258pub fn write_query_index(
259    sections: &[QueryIndexSection],
260    identity: &QueryIndexIdentity,
261    destination: &Path,
262) -> Result<(), QueryIndexError> {
263    let (descriptors, total_bytes) = layout_sections(sections)?;
264    let parent = destination
265        .parent()
266        .ok_or(QueryIndexError::InvalidHeader("destination has no parent"))?;
267    fs::create_dir_all(parent)?;
268    let (temporary, mut file) = loop {
269        let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
270        let temporary = temporary_path(destination, sequence)?;
271        match OpenOptions::new()
272            .write(true)
273            .create_new(true)
274            .open(&temporary)
275        {
276            Ok(file) => break (temporary, file),
277            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
278            Err(error) => return Err(error.into()),
279        }
280    };
281    let result = (|| {
282        file.set_len(total_bytes)?;
283        let by_kind = sections
284            .iter()
285            .map(|section| (section.kind, section))
286            .collect::<std::collections::HashMap<_, _>>();
287        for descriptor in &descriptors {
288            let section = by_kind[&descriptor.kind];
289            file.seek(SeekFrom::Start(descriptor.offset))?;
290            file.write_all(&section.bytes)?;
291            file.seek(SeekFrom::Start(descriptor.digests_offset))?;
292            for page in section.bytes.chunks(QUERY_INDEX_PAGE_SIZE) {
293                file.write_all(&Sha256::digest(page))?;
294            }
295        }
296        file.seek(SeekFrom::Start(0))?;
297        file.write_all(&make_header(identity, &descriptors, total_bytes))?;
298        // Not synced: an index is disposable, and a reader verifies every
299        // page against its digest, so one a machine crash left torn is
300        // rebuilt from the evidence like a missing one. Syncing 90 MB cost
301        // each publication of a 3,900-test run 50 ms.
302        drop(file);
303        fs::rename(&temporary, destination)?;
304        Ok(())
305    })();
306    if result.is_err() {
307        let _ = fs::remove_file(&temporary);
308    }
309    result
310}
311
312pub struct QueryIndex {
313    mmap: Mmap,
314    descriptors: Vec<SectionDescriptor>,
315    verified_pages: Mutex<BTreeSet<(u32, usize)>>,
316}
317
318impl QueryIndex {
319    pub fn open(path: &Path, expected: &QueryIndexIdentity) -> Result<Self, QueryIndexError> {
320        let metadata = fs::symlink_metadata(path)?;
321        if !metadata.is_file() {
322            return Err(QueryIndexError::NotRegularFile(path.to_owned()));
323        }
324        let file = File::open(path)?;
325        // Supercov publishes a new immutable inode by rename and never mutates
326        // the inode while it may be mapped. Readers retain this file handle.
327        let mmap = unsafe { Mmap::map(&file)? };
328        let header = mmap
329            .get(..QUERY_INDEX_HEADER_SIZE)
330            .ok_or(QueryIndexError::InvalidHeader("truncated"))?;
331        if header.get(..8) != Some(MAGIC.as_slice()) {
332            return Err(QueryIndexError::InvalidHeader("magic"));
333        }
334        if get_u32(header, 8)? != QUERY_INDEX_SCHEMA_VERSION {
335            return Err(QueryIndexError::InvalidHeader("schema version"));
336        }
337        if get_u32(header, 12)? as usize != QUERY_INDEX_HEADER_SIZE {
338            return Err(QueryIndexError::InvalidHeader("header size"));
339        }
340        let expected_checksum = Sha256::digest(&header[..HEADER_CHECKSUM_OFFSET]);
341        if header[HEADER_CHECKSUM_OFFSET..] != expected_checksum[..] {
342            return Err(QueryIndexError::InvalidHeader("checksum"));
343        }
344        if get_u64(header, 16)? != mmap.len() as u64 {
345            return Err(QueryIndexError::InvalidHeader("total length"));
346        }
347        if get_u32(header, 40)? as usize != QUERY_INDEX_PAGE_SIZE {
348            return Err(QueryIndexError::InvalidHeader("page size"));
349        }
350        if get_u64(header, 24)? != expected.evidence_bytes {
351            return Err(QueryIndexError::IdentityMismatch("evidence length"));
352        }
353        if get_u32(header, 32)? != expected.archive_schema_version {
354            return Err(QueryIndexError::IdentityMismatch("archive schema"));
355        }
356        if header[48..80] != expected.evidence_sha256 {
357            return Err(QueryIndexError::IdentityMismatch("evidence hash"));
358        }
359        if header[80..112] != expected.analysis_sha256 {
360            return Err(QueryIndexError::IdentityMismatch("analysis hash"));
361        }
362        if header[112..144] != expected.producer_sha256 {
363            return Err(QueryIndexError::IdentityMismatch("producer hash"));
364        }
365        let section_count = get_u32(header, 36)? as usize;
366        if section_count > QUERY_INDEX_MAX_SECTIONS {
367            return Err(QueryIndexError::TooManySections(section_count));
368        }
369        let mut descriptors = Vec::with_capacity(section_count);
370        let mut kinds = HashSet::new();
371        let mut ranges = vec![(0_u64, QUERY_INDEX_HEADER_SIZE as u64)];
372        for index in 0..section_count {
373            let offset = DESCRIPTOR_OFFSET + index * DESCRIPTOR_SIZE;
374            let descriptor = SectionDescriptor {
375                kind: get_u32(header, offset)?,
376                record_size: get_u32(header, offset + 4)?,
377                offset: get_u64(header, offset + 8)?,
378                length: get_u64(header, offset + 16)?,
379                count: get_u64(header, offset + 24)?,
380                digests_offset: get_u64(header, offset + 32)?,
381                digest_count: get_u32(header, offset + 40)?,
382            };
383            if descriptor.kind == 0 || !kinds.insert(descriptor.kind) {
384                return Err(QueryIndexError::DuplicateSection(descriptor.kind));
385            }
386            if descriptor.record_size > 0
387                && u64::from(descriptor.record_size)
388                    .checked_mul(descriptor.count)
389                    .ok_or(QueryIndexError::SizeOverflow)?
390                    != descriptor.length
391            {
392                return Err(QueryIndexError::InvalidSection {
393                    kind: descriptor.kind,
394                    reason: "record shape",
395                });
396            }
397            let digest_count = usize::try_from(descriptor.length)
398                .map_err(|_| QueryIndexError::SizeOverflow)?
399                .div_ceil(QUERY_INDEX_PAGE_SIZE);
400            if descriptor.digest_count as usize != digest_count {
401                return Err(QueryIndexError::InvalidSection {
402                    kind: descriptor.kind,
403                    reason: "digest count",
404                });
405            }
406            let data_end = checked_end(descriptor.offset, descriptor.length)?;
407            let digest_length = u64::from(descriptor.digest_count) * 32;
408            let digest_end = checked_end(descriptor.digests_offset, digest_length)?;
409            if descriptor.offset < QUERY_INDEX_HEADER_SIZE as u64
410                || data_end > mmap.len() as u64
411                || descriptor.digests_offset < QUERY_INDEX_HEADER_SIZE as u64
412                || digest_end > mmap.len() as u64
413            {
414                return Err(QueryIndexError::InvalidSection {
415                    kind: descriptor.kind,
416                    reason: "bounds",
417                });
418            }
419            ranges.push((descriptor.offset, data_end));
420            ranges.push((descriptor.digests_offset, digest_end));
421            descriptors.push(descriptor);
422        }
423        ranges.sort_unstable();
424        if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
425            return Err(QueryIndexError::InvalidHeader("overlapping sections"));
426        }
427        Ok(Self {
428            mmap,
429            descriptors,
430            verified_pages: Mutex::new(BTreeSet::new()),
431        })
432    }
433
434    pub fn descriptor(&self, kind: u32) -> Result<SectionDescriptor, QueryIndexError> {
435        self.descriptors
436            .iter()
437            .find(|descriptor| descriptor.kind == kind)
438            .copied()
439            .ok_or(QueryIndexError::MissingSection(kind))
440    }
441
442    /// Authenticate every section page before a disposable index is accepted.
443    pub fn verify_all(&self) -> Result<(), QueryIndexError> {
444        for descriptor in &self.descriptors {
445            for page in 0..descriptor.digest_count as usize {
446                self.verify_page(*descriptor, page)?;
447            }
448        }
449        Ok(())
450    }
451
452    fn verify_page(
453        &self,
454        descriptor: SectionDescriptor,
455        page: usize,
456    ) -> Result<(), QueryIndexError> {
457        let key = (descriptor.kind, page);
458        if self.verified_pages.lock().unwrap().contains(&key) {
459            return Ok(());
460        }
461        let page_start = usize::try_from(descriptor.offset)
462            .map_err(|_| QueryIndexError::SizeOverflow)?
463            .checked_add(page * QUERY_INDEX_PAGE_SIZE)
464            .ok_or(QueryIndexError::SizeOverflow)?;
465        let section_end = usize::try_from(checked_end(descriptor.offset, descriptor.length)?)
466            .map_err(|_| QueryIndexError::SizeOverflow)?;
467        let page_end = page_start
468            .checked_add(QUERY_INDEX_PAGE_SIZE)
469            .ok_or(QueryIndexError::SizeOverflow)?
470            .min(section_end);
471        let digest_start = usize::try_from(descriptor.digests_offset)
472            .map_err(|_| QueryIndexError::SizeOverflow)?
473            .checked_add(page * 32)
474            .ok_or(QueryIndexError::SizeOverflow)?;
475        let expected =
476            self.mmap
477                .get(digest_start..digest_start + 32)
478                .ok_or(QueryIndexError::CorruptPage {
479                    kind: descriptor.kind,
480                    page,
481                })?;
482        let actual = Sha256::digest(&self.mmap[page_start..page_end]);
483        if actual[..] != expected[..] {
484            return Err(QueryIndexError::CorruptPage {
485                kind: descriptor.kind,
486                page,
487            });
488        }
489        self.verified_pages.lock().unwrap().insert(key);
490        Ok(())
491    }
492
493    pub fn bytes(&self, kind: u32, offset: u64, length: u64) -> Result<&[u8], QueryIndexError> {
494        let descriptor = self.descriptor(kind)?;
495        let end = checked_end(offset, length)?;
496        if end > descriptor.length {
497            return Err(QueryIndexError::OutOfBounds {
498                kind,
499                offset,
500                length,
501            });
502        }
503        if length > 0 {
504            let first = usize::try_from(offset).map_err(|_| QueryIndexError::SizeOverflow)?
505                / QUERY_INDEX_PAGE_SIZE;
506            let last = usize::try_from(end - 1).map_err(|_| QueryIndexError::SizeOverflow)?
507                / QUERY_INDEX_PAGE_SIZE;
508            for page in first..=last {
509                self.verify_page(descriptor, page)?;
510            }
511        }
512        let start = usize::try_from(descriptor.offset + offset)
513            .map_err(|_| QueryIndexError::SizeOverflow)?;
514        let end =
515            usize::try_from(descriptor.offset + end).map_err(|_| QueryIndexError::SizeOverflow)?;
516        Ok(&self.mmap[start..end])
517    }
518
519    pub fn record(&self, kind: u32, index: u64) -> Result<&[u8], QueryIndexError> {
520        let descriptor = self.descriptor(kind)?;
521        if descriptor.record_size == 0 || index >= descriptor.count {
522            return Err(QueryIndexError::OutOfBounds {
523                kind,
524                offset: index,
525                length: 1,
526            });
527        }
528        let offset = index
529            .checked_mul(u64::from(descriptor.record_size))
530            .ok_or(QueryIndexError::SizeOverflow)?;
531        self.bytes(kind, offset, u64::from(descriptor.record_size))
532    }
533}
534
535#[cfg(test)]
536mod tests {
537    use std::time::{SystemTime, UNIX_EPOCH};
538
539    use super::*;
540
541    fn root(label: &str) -> PathBuf {
542        let nonce = SystemTime::now()
543            .duration_since(UNIX_EPOCH)
544            .unwrap()
545            .as_nanos();
546        let path = std::env::temp_dir().join(format!(
547            "supercov-query-index-{label}-{}-{nonce}",
548            std::process::id()
549        ));
550        fs::create_dir_all(&path).unwrap();
551        path
552    }
553
554    fn identity(seed: u8) -> QueryIndexIdentity {
555        QueryIndexIdentity {
556            evidence_sha256: [seed; 32],
557            evidence_bytes: 100 + u64::from(seed),
558            analysis_sha256: [seed.wrapping_add(1); 32],
559            producer_sha256: [seed.wrapping_add(2); 32],
560            archive_schema_version: 2,
561        }
562    }
563
564    fn sections(value: u8) -> Vec<QueryIndexSection> {
565        vec![
566            QueryIndexSection {
567                kind: 2,
568                record_size: 4,
569                count: 2,
570                bytes: vec![value, 1, 2, 3, value, 5, 6, 7],
571            },
572            QueryIndexSection {
573                kind: 1,
574                record_size: 0,
575                count: 3,
576                bytes: vec![8; QUERY_INDEX_PAGE_SIZE + 3],
577            },
578        ]
579    }
580
581    #[test]
582    fn atomically_round_trips_sections_and_checked_records() {
583        let root = root("roundtrip");
584        let path = root.join("query-index.v1.bin");
585        write_query_index(&sections(9), &identity(1), &path).unwrap();
586        let index = QueryIndex::open(&path, &identity(1)).unwrap();
587        assert_eq!(index.record(2, 0).unwrap(), &[9, 1, 2, 3]);
588        assert_eq!(index.record(2, 1).unwrap(), &[9, 5, 6, 7]);
589        assert_eq!(
590            index.bytes(1, QUERY_INDEX_PAGE_SIZE as u64 - 1, 4).unwrap(),
591            &[8; 4]
592        );
593        assert!(matches!(
594            index.record(2, 2),
595            Err(QueryIndexError::OutOfBounds { .. })
596        ));
597        assert!(fs::read_dir(&root).unwrap().all(|entry| {
598            !entry
599                .unwrap()
600                .file_name()
601                .to_string_lossy()
602                .ends_with(".tmp")
603        }));
604        fs::remove_dir_all(root).unwrap();
605    }
606
607    #[test]
608    fn rejects_stale_identity_and_payload_corruption() {
609        let root = root("corrupt");
610        let path = root.join("query-index.v1.bin");
611        write_query_index(&sections(9), &identity(1), &path).unwrap();
612        assert!(matches!(
613            QueryIndex::open(&path, &identity(2)),
614            Err(QueryIndexError::IdentityMismatch(_))
615        ));
616        let descriptor = QueryIndex::open(&path, &identity(1))
617            .unwrap()
618            .descriptor(2)
619            .unwrap();
620        let mut file = OpenOptions::new().write(true).open(&path).unwrap();
621        file.seek(SeekFrom::Start(descriptor.offset)).unwrap();
622        file.write_all(&[0xff]).unwrap();
623        file.sync_all().unwrap();
624        let index = QueryIndex::open(&path, &identity(1)).unwrap();
625        assert!(matches!(
626            index.record(2, 0),
627            Err(QueryIndexError::CorruptPage { kind: 2, page: 0 })
628        ));
629        fs::remove_dir_all(root).unwrap();
630    }
631
632    #[test]
633    fn replacement_never_invalidates_an_open_mapping() {
634        let root = root("replacement");
635        let path = root.join("query-index.v1.bin");
636        write_query_index(&sections(1), &identity(1), &path).unwrap();
637        let old = QueryIndex::open(&path, &identity(1)).unwrap();
638        write_query_index(&sections(2), &identity(2), &path).unwrap();
639        assert_eq!(old.record(2, 0).unwrap()[0], 1);
640        assert_eq!(
641            QueryIndex::open(&path, &identity(2))
642                .unwrap()
643                .record(2, 0)
644                .unwrap()[0],
645            2
646        );
647        fs::remove_dir_all(root).unwrap();
648    }
649
650    #[test]
651    fn rejects_duplicate_and_misshapen_sections_without_publication() {
652        let root = root("invalid");
653        let path = root.join("query-index.v1.bin");
654        let duplicate = vec![
655            QueryIndexSection {
656                kind: 1,
657                record_size: 1,
658                count: 1,
659                bytes: vec![1],
660            },
661            QueryIndexSection {
662                kind: 1,
663                record_size: 1,
664                count: 1,
665                bytes: vec![2],
666            },
667        ];
668        assert!(matches!(
669            write_query_index(&duplicate, &identity(1), &path),
670            Err(QueryIndexError::DuplicateSection(1))
671        ));
672        assert!(!path.exists());
673        assert!(matches!(
674            write_query_index(
675                &[QueryIndexSection {
676                    kind: 1,
677                    record_size: 4,
678                    count: 2,
679                    bytes: vec![1]
680                }],
681                &identity(1),
682                &path
683            ),
684            Err(QueryIndexError::InvalidSection { .. })
685        ));
686        fs::remove_dir_all(root).unwrap();
687    }
688
689    #[cfg(unix)]
690    #[test]
691    fn rejects_symlinked_indexes() {
692        use std::os::unix::fs::symlink;
693
694        let root = root("symlink");
695        let path = root.join("query-index.v1.bin");
696        let link = root.join("linked.bin");
697        write_query_index(&sections(1), &identity(1), &path).unwrap();
698        symlink(&path, &link).unwrap();
699        assert!(matches!(
700            QueryIndex::open(&link, &identity(1)),
701            Err(QueryIndexError::NotRegularFile(_))
702        ));
703        fs::remove_dir_all(root).unwrap();
704    }
705}