1use 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(§ion.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 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 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 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(§ions(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(§ions(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(§ions(1), &identity(1), &path).unwrap();
637 let old = QueryIndex::open(&path, &identity(1)).unwrap();
638 write_query_index(§ions(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(§ions(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}