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 = 2;
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 sync_parent(path: &Path) {
169    if let Some(parent) = path.parent()
170        && let Ok(directory) = File::open(parent)
171    {
172        let _ = directory.sync_all();
173    }
174}
175
176fn layout_sections(
177    sections: &[QueryIndexSection],
178) -> Result<(Vec<SectionDescriptor>, u64), QueryIndexError> {
179    if sections.len() > QUERY_INDEX_MAX_SECTIONS {
180        return Err(QueryIndexError::TooManySections(sections.len()));
181    }
182    let mut seen = HashSet::new();
183    let mut ordered = sections.iter().collect::<Vec<_>>();
184    ordered.sort_by_key(|section| section.kind);
185    for section in &ordered {
186        if section.kind == 0 {
187            return Err(QueryIndexError::InvalidSection {
188                kind: 0,
189                reason: "kind zero is reserved",
190            });
191        }
192        if !seen.insert(section.kind) {
193            return Err(QueryIndexError::DuplicateSection(section.kind));
194        }
195        if section.record_size > 0
196            && u64::from(section.record_size)
197                .checked_mul(section.count)
198                .ok_or(QueryIndexError::SizeOverflow)?
199                != section.bytes.len() as u64
200        {
201            return Err(QueryIndexError::InvalidSection {
202                kind: section.kind,
203                reason: "record size times count does not equal section length",
204            });
205        }
206    }
207    let mut cursor = QUERY_INDEX_HEADER_SIZE as u64;
208    let mut descriptors = Vec::with_capacity(ordered.len());
209    for section in &ordered {
210        cursor = align(cursor, 8)?;
211        let length =
212            u64::try_from(section.bytes.len()).map_err(|_| QueryIndexError::SizeOverflow)?;
213        let offset = cursor;
214        cursor = checked_end(cursor, length)?;
215        descriptors.push(SectionDescriptor {
216            kind: section.kind,
217            record_size: section.record_size,
218            offset,
219            length,
220            count: section.count,
221            digests_offset: 0,
222            digest_count: u32::try_from(section.bytes.len().div_ceil(QUERY_INDEX_PAGE_SIZE))
223                .map_err(|_| QueryIndexError::SizeOverflow)?,
224        });
225    }
226    for descriptor in &mut descriptors {
227        cursor = align(cursor, 8)?;
228        descriptor.digests_offset = cursor;
229        cursor = checked_end(cursor, u64::from(descriptor.digest_count) * 32)?;
230    }
231    Ok((descriptors, cursor))
232}
233
234fn make_header(
235    identity: &QueryIndexIdentity,
236    descriptors: &[SectionDescriptor],
237    total_bytes: u64,
238) -> Vec<u8> {
239    let mut header = vec![0_u8; QUERY_INDEX_HEADER_SIZE];
240    header[..8].copy_from_slice(MAGIC);
241    put_u32(&mut header, 8, QUERY_INDEX_SCHEMA_VERSION);
242    put_u32(&mut header, 12, QUERY_INDEX_HEADER_SIZE as u32);
243    put_u64(&mut header, 16, total_bytes);
244    put_u64(&mut header, 24, identity.evidence_bytes);
245    put_u32(&mut header, 32, identity.archive_schema_version);
246    put_u32(&mut header, 36, descriptors.len() as u32);
247    put_u32(&mut header, 40, QUERY_INDEX_PAGE_SIZE as u32);
248    header[48..80].copy_from_slice(&identity.evidence_sha256);
249    header[80..112].copy_from_slice(&identity.analysis_sha256);
250    header[112..144].copy_from_slice(&identity.producer_sha256);
251    for (index, descriptor) in descriptors.iter().enumerate() {
252        let offset = DESCRIPTOR_OFFSET + index * DESCRIPTOR_SIZE;
253        put_u32(&mut header, offset, descriptor.kind);
254        put_u32(&mut header, offset + 4, descriptor.record_size);
255        put_u64(&mut header, offset + 8, descriptor.offset);
256        put_u64(&mut header, offset + 16, descriptor.length);
257        put_u64(&mut header, offset + 24, descriptor.count);
258        put_u64(&mut header, offset + 32, descriptor.digests_offset);
259        put_u32(&mut header, offset + 40, descriptor.digest_count);
260    }
261    let checksum = Sha256::digest(&header[..HEADER_CHECKSUM_OFFSET]);
262    header[HEADER_CHECKSUM_OFFSET..].copy_from_slice(&checksum);
263    header
264}
265
266pub fn write_query_index(
267    sections: &[QueryIndexSection],
268    identity: &QueryIndexIdentity,
269    destination: &Path,
270) -> Result<(), QueryIndexError> {
271    let (descriptors, total_bytes) = layout_sections(sections)?;
272    let parent = destination
273        .parent()
274        .ok_or(QueryIndexError::InvalidHeader("destination has no parent"))?;
275    fs::create_dir_all(parent)?;
276    let (temporary, mut file) = loop {
277        let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
278        let temporary = temporary_path(destination, sequence)?;
279        match OpenOptions::new()
280            .write(true)
281            .create_new(true)
282            .open(&temporary)
283        {
284            Ok(file) => break (temporary, file),
285            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
286            Err(error) => return Err(error.into()),
287        }
288    };
289    let result = (|| {
290        file.set_len(total_bytes)?;
291        let by_kind = sections
292            .iter()
293            .map(|section| (section.kind, section))
294            .collect::<std::collections::HashMap<_, _>>();
295        for descriptor in &descriptors {
296            let section = by_kind[&descriptor.kind];
297            file.seek(SeekFrom::Start(descriptor.offset))?;
298            file.write_all(&section.bytes)?;
299            file.seek(SeekFrom::Start(descriptor.digests_offset))?;
300            for page in section.bytes.chunks(QUERY_INDEX_PAGE_SIZE) {
301                file.write_all(&Sha256::digest(page))?;
302            }
303        }
304        file.seek(SeekFrom::Start(0))?;
305        file.write_all(&make_header(identity, &descriptors, total_bytes))?;
306        file.sync_all()?;
307        drop(file);
308        fs::rename(&temporary, destination)?;
309        sync_parent(destination);
310        Ok(())
311    })();
312    if result.is_err() {
313        let _ = fs::remove_file(&temporary);
314    }
315    result
316}
317
318pub struct QueryIndex {
319    mmap: Mmap,
320    descriptors: Vec<SectionDescriptor>,
321    verified_pages: Mutex<BTreeSet<(u32, usize)>>,
322}
323
324impl QueryIndex {
325    pub fn open(path: &Path, expected: &QueryIndexIdentity) -> Result<Self, QueryIndexError> {
326        let metadata = fs::symlink_metadata(path)?;
327        if !metadata.is_file() {
328            return Err(QueryIndexError::NotRegularFile(path.to_owned()));
329        }
330        let file = File::open(path)?;
331        // Supercov publishes a new immutable inode by rename and never mutates
332        // the inode while it may be mapped. Readers retain this file handle.
333        let mmap = unsafe { Mmap::map(&file)? };
334        let header = mmap
335            .get(..QUERY_INDEX_HEADER_SIZE)
336            .ok_or(QueryIndexError::InvalidHeader("truncated"))?;
337        if header.get(..8) != Some(MAGIC.as_slice()) {
338            return Err(QueryIndexError::InvalidHeader("magic"));
339        }
340        if get_u32(header, 8)? != QUERY_INDEX_SCHEMA_VERSION {
341            return Err(QueryIndexError::InvalidHeader("schema version"));
342        }
343        if get_u32(header, 12)? as usize != QUERY_INDEX_HEADER_SIZE {
344            return Err(QueryIndexError::InvalidHeader("header size"));
345        }
346        let expected_checksum = Sha256::digest(&header[..HEADER_CHECKSUM_OFFSET]);
347        if header[HEADER_CHECKSUM_OFFSET..] != expected_checksum[..] {
348            return Err(QueryIndexError::InvalidHeader("checksum"));
349        }
350        if get_u64(header, 16)? != mmap.len() as u64 {
351            return Err(QueryIndexError::InvalidHeader("total length"));
352        }
353        if get_u32(header, 40)? as usize != QUERY_INDEX_PAGE_SIZE {
354            return Err(QueryIndexError::InvalidHeader("page size"));
355        }
356        if get_u64(header, 24)? != expected.evidence_bytes {
357            return Err(QueryIndexError::IdentityMismatch("evidence length"));
358        }
359        if get_u32(header, 32)? != expected.archive_schema_version {
360            return Err(QueryIndexError::IdentityMismatch("archive schema"));
361        }
362        if header[48..80] != expected.evidence_sha256 {
363            return Err(QueryIndexError::IdentityMismatch("evidence hash"));
364        }
365        if header[80..112] != expected.analysis_sha256 {
366            return Err(QueryIndexError::IdentityMismatch("analysis hash"));
367        }
368        if header[112..144] != expected.producer_sha256 {
369            return Err(QueryIndexError::IdentityMismatch("producer hash"));
370        }
371        let section_count = get_u32(header, 36)? as usize;
372        if section_count > QUERY_INDEX_MAX_SECTIONS {
373            return Err(QueryIndexError::TooManySections(section_count));
374        }
375        let mut descriptors = Vec::with_capacity(section_count);
376        let mut kinds = HashSet::new();
377        let mut ranges = vec![(0_u64, QUERY_INDEX_HEADER_SIZE as u64)];
378        for index in 0..section_count {
379            let offset = DESCRIPTOR_OFFSET + index * DESCRIPTOR_SIZE;
380            let descriptor = SectionDescriptor {
381                kind: get_u32(header, offset)?,
382                record_size: get_u32(header, offset + 4)?,
383                offset: get_u64(header, offset + 8)?,
384                length: get_u64(header, offset + 16)?,
385                count: get_u64(header, offset + 24)?,
386                digests_offset: get_u64(header, offset + 32)?,
387                digest_count: get_u32(header, offset + 40)?,
388            };
389            if descriptor.kind == 0 || !kinds.insert(descriptor.kind) {
390                return Err(QueryIndexError::DuplicateSection(descriptor.kind));
391            }
392            if descriptor.record_size > 0
393                && u64::from(descriptor.record_size)
394                    .checked_mul(descriptor.count)
395                    .ok_or(QueryIndexError::SizeOverflow)?
396                    != descriptor.length
397            {
398                return Err(QueryIndexError::InvalidSection {
399                    kind: descriptor.kind,
400                    reason: "record shape",
401                });
402            }
403            let digest_count = usize::try_from(descriptor.length)
404                .map_err(|_| QueryIndexError::SizeOverflow)?
405                .div_ceil(QUERY_INDEX_PAGE_SIZE);
406            if descriptor.digest_count as usize != digest_count {
407                return Err(QueryIndexError::InvalidSection {
408                    kind: descriptor.kind,
409                    reason: "digest count",
410                });
411            }
412            let data_end = checked_end(descriptor.offset, descriptor.length)?;
413            let digest_length = u64::from(descriptor.digest_count) * 32;
414            let digest_end = checked_end(descriptor.digests_offset, digest_length)?;
415            if descriptor.offset < QUERY_INDEX_HEADER_SIZE as u64
416                || data_end > mmap.len() as u64
417                || descriptor.digests_offset < QUERY_INDEX_HEADER_SIZE as u64
418                || digest_end > mmap.len() as u64
419            {
420                return Err(QueryIndexError::InvalidSection {
421                    kind: descriptor.kind,
422                    reason: "bounds",
423                });
424            }
425            ranges.push((descriptor.offset, data_end));
426            ranges.push((descriptor.digests_offset, digest_end));
427            descriptors.push(descriptor);
428        }
429        ranges.sort_unstable();
430        if ranges.windows(2).any(|pair| pair[0].1 > pair[1].0) {
431            return Err(QueryIndexError::InvalidHeader("overlapping sections"));
432        }
433        Ok(Self {
434            mmap,
435            descriptors,
436            verified_pages: Mutex::new(BTreeSet::new()),
437        })
438    }
439
440    pub fn descriptor(&self, kind: u32) -> Result<SectionDescriptor, QueryIndexError> {
441        self.descriptors
442            .iter()
443            .find(|descriptor| descriptor.kind == kind)
444            .copied()
445            .ok_or(QueryIndexError::MissingSection(kind))
446    }
447
448    /// Authenticate every section page before a disposable index is accepted.
449    pub fn verify_all(&self) -> Result<(), QueryIndexError> {
450        for descriptor in &self.descriptors {
451            for page in 0..descriptor.digest_count as usize {
452                self.verify_page(*descriptor, page)?;
453            }
454        }
455        Ok(())
456    }
457
458    fn verify_page(
459        &self,
460        descriptor: SectionDescriptor,
461        page: usize,
462    ) -> Result<(), QueryIndexError> {
463        let key = (descriptor.kind, page);
464        if self.verified_pages.lock().unwrap().contains(&key) {
465            return Ok(());
466        }
467        let page_start = usize::try_from(descriptor.offset)
468            .map_err(|_| QueryIndexError::SizeOverflow)?
469            .checked_add(page * QUERY_INDEX_PAGE_SIZE)
470            .ok_or(QueryIndexError::SizeOverflow)?;
471        let section_end = usize::try_from(checked_end(descriptor.offset, descriptor.length)?)
472            .map_err(|_| QueryIndexError::SizeOverflow)?;
473        let page_end = page_start
474            .checked_add(QUERY_INDEX_PAGE_SIZE)
475            .ok_or(QueryIndexError::SizeOverflow)?
476            .min(section_end);
477        let digest_start = usize::try_from(descriptor.digests_offset)
478            .map_err(|_| QueryIndexError::SizeOverflow)?
479            .checked_add(page * 32)
480            .ok_or(QueryIndexError::SizeOverflow)?;
481        let expected =
482            self.mmap
483                .get(digest_start..digest_start + 32)
484                .ok_or(QueryIndexError::CorruptPage {
485                    kind: descriptor.kind,
486                    page,
487                })?;
488        let actual = Sha256::digest(&self.mmap[page_start..page_end]);
489        if actual[..] != expected[..] {
490            return Err(QueryIndexError::CorruptPage {
491                kind: descriptor.kind,
492                page,
493            });
494        }
495        self.verified_pages.lock().unwrap().insert(key);
496        Ok(())
497    }
498
499    pub fn bytes(&self, kind: u32, offset: u64, length: u64) -> Result<&[u8], QueryIndexError> {
500        let descriptor = self.descriptor(kind)?;
501        let end = checked_end(offset, length)?;
502        if end > descriptor.length {
503            return Err(QueryIndexError::OutOfBounds {
504                kind,
505                offset,
506                length,
507            });
508        }
509        if length > 0 {
510            let first = usize::try_from(offset).map_err(|_| QueryIndexError::SizeOverflow)?
511                / QUERY_INDEX_PAGE_SIZE;
512            let last = usize::try_from(end - 1).map_err(|_| QueryIndexError::SizeOverflow)?
513                / QUERY_INDEX_PAGE_SIZE;
514            for page in first..=last {
515                self.verify_page(descriptor, page)?;
516            }
517        }
518        let start = usize::try_from(descriptor.offset + offset)
519            .map_err(|_| QueryIndexError::SizeOverflow)?;
520        let end =
521            usize::try_from(descriptor.offset + end).map_err(|_| QueryIndexError::SizeOverflow)?;
522        Ok(&self.mmap[start..end])
523    }
524
525    pub fn record(&self, kind: u32, index: u64) -> Result<&[u8], QueryIndexError> {
526        let descriptor = self.descriptor(kind)?;
527        if descriptor.record_size == 0 || index >= descriptor.count {
528            return Err(QueryIndexError::OutOfBounds {
529                kind,
530                offset: index,
531                length: 1,
532            });
533        }
534        let offset = index
535            .checked_mul(u64::from(descriptor.record_size))
536            .ok_or(QueryIndexError::SizeOverflow)?;
537        self.bytes(kind, offset, u64::from(descriptor.record_size))
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    use std::time::{SystemTime, UNIX_EPOCH};
544
545    use super::*;
546
547    fn root(label: &str) -> PathBuf {
548        let nonce = SystemTime::now()
549            .duration_since(UNIX_EPOCH)
550            .unwrap()
551            .as_nanos();
552        let path = std::env::temp_dir().join(format!(
553            "supercov-query-index-{label}-{}-{nonce}",
554            std::process::id()
555        ));
556        fs::create_dir_all(&path).unwrap();
557        path
558    }
559
560    fn identity(seed: u8) -> QueryIndexIdentity {
561        QueryIndexIdentity {
562            evidence_sha256: [seed; 32],
563            evidence_bytes: 100 + u64::from(seed),
564            analysis_sha256: [seed.wrapping_add(1); 32],
565            producer_sha256: [seed.wrapping_add(2); 32],
566            archive_schema_version: 2,
567        }
568    }
569
570    fn sections(value: u8) -> Vec<QueryIndexSection> {
571        vec![
572            QueryIndexSection {
573                kind: 2,
574                record_size: 4,
575                count: 2,
576                bytes: vec![value, 1, 2, 3, value, 5, 6, 7],
577            },
578            QueryIndexSection {
579                kind: 1,
580                record_size: 0,
581                count: 3,
582                bytes: vec![8; QUERY_INDEX_PAGE_SIZE + 3],
583            },
584        ]
585    }
586
587    #[test]
588    fn atomically_round_trips_sections_and_checked_records() {
589        let root = root("roundtrip");
590        let path = root.join("query-index.v1.bin");
591        write_query_index(&sections(9), &identity(1), &path).unwrap();
592        let index = QueryIndex::open(&path, &identity(1)).unwrap();
593        assert_eq!(index.record(2, 0).unwrap(), &[9, 1, 2, 3]);
594        assert_eq!(index.record(2, 1).unwrap(), &[9, 5, 6, 7]);
595        assert_eq!(
596            index.bytes(1, QUERY_INDEX_PAGE_SIZE as u64 - 1, 4).unwrap(),
597            &[8; 4]
598        );
599        assert!(matches!(
600            index.record(2, 2),
601            Err(QueryIndexError::OutOfBounds { .. })
602        ));
603        assert!(fs::read_dir(&root).unwrap().all(|entry| {
604            !entry
605                .unwrap()
606                .file_name()
607                .to_string_lossy()
608                .ends_with(".tmp")
609        }));
610        fs::remove_dir_all(root).unwrap();
611    }
612
613    #[test]
614    fn rejects_stale_identity_and_payload_corruption() {
615        let root = root("corrupt");
616        let path = root.join("query-index.v1.bin");
617        write_query_index(&sections(9), &identity(1), &path).unwrap();
618        assert!(matches!(
619            QueryIndex::open(&path, &identity(2)),
620            Err(QueryIndexError::IdentityMismatch(_))
621        ));
622        let descriptor = QueryIndex::open(&path, &identity(1))
623            .unwrap()
624            .descriptor(2)
625            .unwrap();
626        let mut file = OpenOptions::new().write(true).open(&path).unwrap();
627        file.seek(SeekFrom::Start(descriptor.offset)).unwrap();
628        file.write_all(&[0xff]).unwrap();
629        file.sync_all().unwrap();
630        let index = QueryIndex::open(&path, &identity(1)).unwrap();
631        assert!(matches!(
632            index.record(2, 0),
633            Err(QueryIndexError::CorruptPage { kind: 2, page: 0 })
634        ));
635        fs::remove_dir_all(root).unwrap();
636    }
637
638    #[test]
639    fn replacement_never_invalidates_an_open_mapping() {
640        let root = root("replacement");
641        let path = root.join("query-index.v1.bin");
642        write_query_index(&sections(1), &identity(1), &path).unwrap();
643        let old = QueryIndex::open(&path, &identity(1)).unwrap();
644        write_query_index(&sections(2), &identity(2), &path).unwrap();
645        assert_eq!(old.record(2, 0).unwrap()[0], 1);
646        assert_eq!(
647            QueryIndex::open(&path, &identity(2))
648                .unwrap()
649                .record(2, 0)
650                .unwrap()[0],
651            2
652        );
653        fs::remove_dir_all(root).unwrap();
654    }
655
656    #[test]
657    fn rejects_duplicate_and_misshapen_sections_without_publication() {
658        let root = root("invalid");
659        let path = root.join("query-index.v1.bin");
660        let duplicate = vec![
661            QueryIndexSection {
662                kind: 1,
663                record_size: 1,
664                count: 1,
665                bytes: vec![1],
666            },
667            QueryIndexSection {
668                kind: 1,
669                record_size: 1,
670                count: 1,
671                bytes: vec![2],
672            },
673        ];
674        assert!(matches!(
675            write_query_index(&duplicate, &identity(1), &path),
676            Err(QueryIndexError::DuplicateSection(1))
677        ));
678        assert!(!path.exists());
679        assert!(matches!(
680            write_query_index(
681                &[QueryIndexSection {
682                    kind: 1,
683                    record_size: 4,
684                    count: 2,
685                    bytes: vec![1]
686                }],
687                &identity(1),
688                &path
689            ),
690            Err(QueryIndexError::InvalidSection { .. })
691        ));
692        fs::remove_dir_all(root).unwrap();
693    }
694
695    #[cfg(unix)]
696    #[test]
697    fn rejects_symlinked_indexes() {
698        use std::os::unix::fs::symlink;
699
700        let root = root("symlink");
701        let path = root.join("query-index.v1.bin");
702        let link = root.join("linked.bin");
703        write_query_index(&sections(1), &identity(1), &path).unwrap();
704        symlink(&path, &link).unwrap();
705        assert!(matches!(
706            QueryIndex::open(&link, &identity(1)),
707            Err(QueryIndexError::NotRegularFile(_))
708        ));
709        fs::remove_dir_all(root).unwrap();
710    }
711}