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