1use std::fs::{File, OpenOptions};
2#[cfg(unix)]
3use std::os::unix::fs::FileExt;
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::core::{FieldId, LuciError, Result, SegmentId};
8
9use crate::storage::allocator::BlockAllocator;
10use crate::storage::block::{BLOCK_SIZE, Extent, HEADER_SIZE};
11use crate::storage::directory::{MetadataSnapshot, SegmentEntry, VectorIndexEntry};
12use crate::storage::header::{FileHeader, RootPointer, xxh3_checksum};
13
14#[cfg(unix)]
27pub struct SingleFileDirectory {
28 file: Arc<File>,
29 lock: crate::storage::lock::FileLock,
30 header: FileHeader,
31 allocator: BlockAllocator,
32 committed: MetadataSnapshot,
34 pending_segments: Vec<SegmentEntry>,
36 pending_removals: Vec<SegmentId>,
38 pending_vector_indexes: Vec<VectorIndexEntry>,
42 pending_vector_index_removals: Vec<FieldId>,
44 generation: u64,
46 metadata_dirty: bool,
48 write_timeout: std::time::Duration,
50}
51
52#[cfg(unix)]
53impl SingleFileDirectory {
54 pub fn create(path: impl AsRef<Path>) -> Result<Self> {
58 let file = OpenOptions::new()
59 .read(true)
60 .write(true)
61 .create_new(true)
62 .open(path.as_ref())?;
63
64 let mut lock = crate::storage::lock::FileLock::new(&file);
66 lock.lock_shared()?;
67
68 let file = Arc::new(file);
69 let header = FileHeader::new();
70 file.write_all_at(&header.to_bytes(), 0)?;
71 file.sync_all()?;
72
73 Ok(Self {
74 file,
75 lock,
76 header,
77 allocator: BlockAllocator::new(),
78 committed: MetadataSnapshot::empty(),
79 pending_segments: Vec::new(),
80 pending_removals: Vec::new(),
81 pending_vector_indexes: Vec::new(),
82 pending_vector_index_removals: Vec::new(),
83 generation: 0,
84 metadata_dirty: false,
85 write_timeout: std::time::Duration::from_secs(5),
86 })
87 }
88
89 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
98 let file = OpenOptions::new()
99 .read(true)
100 .write(true)
101 .open(path.as_ref())?;
102
103 let mut lock = crate::storage::lock::FileLock::new(&file);
105 lock.lock_shared()?;
106
107 let file = Arc::new(file);
108
109 let mut header_buf = [0u8; HEADER_SIZE as usize];
111 file.read_exact_at(&mut header_buf, 0)?;
112 let mut header = FileHeader::from_bytes(&header_buf)?;
113
114 let (committed, used_inactive) = load_metadata(&file, &header)?;
116
117 if used_inactive {
120 header.active_root = header.active_root.inactive();
121 file.write_all_at(&header.to_bytes(), 0)?;
122 file.sync_all()?;
123 }
124
125 let allocator =
127 BlockAllocator::from_state(committed.total_blocks, committed.free_list.clone());
128
129 let generation = committed
130 .segments
131 .iter()
132 .map(|s| s.generation)
133 .max()
134 .unwrap_or(0);
135
136 Ok(Self {
137 file,
138 lock,
139 header,
140 allocator,
141 committed,
142 pending_segments: Vec::new(),
143 pending_removals: Vec::new(),
144 pending_vector_indexes: Vec::new(),
145 pending_vector_index_removals: Vec::new(),
146 generation,
147 metadata_dirty: false,
148 write_timeout: std::time::Duration::from_secs(5),
149 })
150 }
151
152 pub fn file_handle(&self) -> Arc<File> {
160 self.file.clone()
161 }
162
163 pub fn set_write_timeout(&mut self, timeout: std::time::Duration) {
169 self.write_timeout = timeout;
170 }
171
172 pub fn open_from_handle(file: Arc<File>) -> Result<Self> {
179 let mut header_buf = [0u8; HEADER_SIZE as usize];
180 file.read_exact_at(&mut header_buf, 0)?;
181 let header = FileHeader::from_bytes(&header_buf)?;
182
183 let (committed, _used_inactive) = load_metadata(&file, &header)?;
184 let allocator =
187 BlockAllocator::from_state(committed.total_blocks, committed.free_list.clone());
188
189 let generation = committed
190 .segments
191 .iter()
192 .map(|s| s.generation)
193 .max()
194 .unwrap_or(0);
195
196 let lock = crate::storage::lock::FileLock::new(&file);
199
200 Ok(Self {
201 file,
202 lock,
203 header,
204 allocator,
205 committed,
206 pending_segments: Vec::new(),
207 pending_removals: Vec::new(),
208 pending_vector_indexes: Vec::new(),
209 pending_vector_index_removals: Vec::new(),
210 generation,
211 metadata_dirty: false,
212 write_timeout: std::time::Duration::from_secs(5),
213 })
214 }
215
216 fn begin_write(&mut self) -> Result<()> {
229 if self.lock.level() >= crate::storage::lock::LockLevel::Reserved {
230 return Ok(()); }
232
233 self.lock.lock_reserved(self.write_timeout)?;
234
235 let mut header_buf = [0u8; HEADER_SIZE as usize];
238 self.file.read_exact_at(&mut header_buf, 0)?;
239 self.header = FileHeader::from_bytes(&header_buf)?;
240
241 let (committed, _used_inactive) = load_metadata(&self.file, &self.header)?;
242 self.allocator =
243 BlockAllocator::from_state(committed.total_blocks, committed.free_list.clone());
244 self.generation = committed
245 .segments
246 .iter()
247 .map(|s| s.generation)
248 .max()
249 .unwrap_or(0);
250 self.committed = committed;
251
252 Ok(())
253 }
254
255 pub fn write_segment(&mut self, segment_id: SegmentId, data: &[u8]) -> Result<()> {
269 if data.is_empty() {
270 return Err(LuciError::InvalidQuery("cannot write empty segment".into()));
271 }
272
273 self.begin_write()?;
274
275 let blocks_needed =
276 ((data.len() as u64 + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64) as u32;
277 let extent = self.allocator.allocate(blocks_needed)?;
278
279 self.file.write_all_at(data, extent.start.byte_offset())?;
280
281 self.pending_segments.push(SegmentEntry::new(
282 segment_id,
283 extent,
284 self.generation + 1,
285 data.len() as u64,
286 ));
287
288 Ok(())
289 }
290
291 pub fn read_segment(&self, segment_id: SegmentId) -> Result<Vec<u8>> {
296 let entry = self
297 .committed
298 .segments
299 .iter()
300 .find(|e| e.segment_id == segment_id)
301 .ok_or_else(|| LuciError::IndexNotFound(format!("segment {segment_id}")))?;
302
303 let mut buf = vec![0u8; entry.data_len as usize];
304 self.file
305 .read_exact_at(&mut buf, entry.extent.start.byte_offset())?;
306 Ok(buf)
307 }
308
309 pub fn commit(&mut self) -> Result<()> {
324 if self.pending_segments.is_empty()
325 && self.pending_removals.is_empty()
326 && self.pending_vector_indexes.is_empty()
327 && self.pending_vector_index_removals.is_empty()
328 && !self.metadata_dirty
329 {
330 return Ok(());
331 }
332
333 let staged_user_metadata = self.committed.user_metadata.clone();
338
339 self.begin_write()?;
349 self.committed.user_metadata = staged_user_metadata;
350
351 self.metadata_dirty = false;
352
353 self.generation += 1;
354
355 let old_inactive_meta = self.header.inactive_root_pointer().block_id;
358 if let Some(block_id) = old_inactive_meta {
359 self.allocator.free(Extent::new(block_id, 1));
360 }
361
362 let meta_extent = self.allocator.allocate(1)?;
364 let meta_block = meta_extent.start;
365
366 let mut segments = self.committed.segments.clone();
368 segments.extend(self.pending_segments.drain(..));
369
370 if !self.pending_removals.is_empty() {
372 segments.retain(|entry| {
373 if self.pending_removals.contains(&entry.segment_id) {
374 self.allocator.free(entry.extent);
375 false
376 } else {
377 true
378 }
379 });
380 self.pending_removals.clear();
381 }
382
383 let mut vector_indexes = self.committed.vector_indexes.clone();
390 if !self.pending_vector_index_removals.is_empty() {
391 vector_indexes.retain(|entry| {
392 if self.pending_vector_index_removals.contains(&entry.field_id) {
393 self.allocator.free(entry.extent);
394 false
395 } else {
396 true
397 }
398 });
399 self.pending_vector_index_removals.clear();
400 }
401 for pending in self.pending_vector_indexes.drain(..) {
402 if let Some(pos) = vector_indexes
403 .iter()
404 .position(|e| e.field_id == pending.field_id)
405 {
406 self.allocator.free(vector_indexes[pos].extent);
407 vector_indexes.remove(pos);
408 }
409 vector_indexes.push(pending);
410 }
411
412 let snapshot = MetadataSnapshot {
413 segments,
414 vector_indexes,
415 total_blocks: self.allocator.total_blocks(),
416 free_list: self.allocator.free_list().to_vec(),
417 user_metadata: self.committed.user_metadata.clone(),
418 };
419
420 assert!(
421 snapshot.fits_in_single_block(),
422 "metadata overflow chaining not yet implemented"
423 );
424
425 let meta_bytes = snapshot.to_bytes();
427 let mut block_buf = vec![0u8; BLOCK_SIZE as usize];
428 block_buf[..meta_bytes.len()].copy_from_slice(&meta_bytes);
429
430 self.file
432 .write_all_at(&block_buf, meta_block.byte_offset())?;
433
434 let checksum = xxh3_checksum(&block_buf);
436 self.header.commit(meta_block, checksum);
437
438 self.file.sync_all()?;
440
441 self.lock.lock_exclusive()?;
443
444 self.file.write_all_at(&self.header.to_bytes(), 0)?;
446
447 self.file.sync_all()?;
449
450 self.lock.downgrade_to_shared()?;
452
453 self.committed = snapshot;
455
456 Ok(())
457 }
458
459 pub fn segments(&self) -> &[SegmentEntry] {
461 &self.committed.segments
462 }
463
464 pub fn generation(&self) -> u64 {
466 self.generation
467 }
468
469 pub fn set_user_metadata(&mut self, metadata: Vec<u8>) {
471 self.committed.user_metadata = metadata;
472 self.metadata_dirty = true;
473 }
474
475 pub fn user_metadata(&self) -> &[u8] {
477 &self.committed.user_metadata
478 }
479
480 pub fn free_block_count(&self) -> u64 {
484 self.allocator.free_block_count()
485 }
486
487 pub fn total_blocks(&self) -> u64 {
491 self.allocator.total_blocks()
492 }
493
494 pub fn remove_segments(&mut self, segment_ids: &[SegmentId]) {
496 self.pending_removals.extend_from_slice(segment_ids);
497 }
498
499 pub fn write_vector_index(&mut self, field_id: FieldId, data: &[u8]) -> Result<()> {
504 if data.is_empty() {
505 return Err(LuciError::InvalidQuery(
506 "cannot write empty vector index".into(),
507 ));
508 }
509
510 self.begin_write()?;
511
512 let blocks_needed =
513 ((data.len() as u64 + BLOCK_SIZE as u64 - 1) / BLOCK_SIZE as u64) as u32;
514 let extent = self.allocator.allocate(blocks_needed)?;
515
516 self.file.write_all_at(data, extent.start.byte_offset())?;
517
518 self.pending_vector_indexes
521 .retain(|e| e.field_id != field_id);
522 self.pending_vector_indexes.push(VectorIndexEntry::new(
523 field_id,
524 extent,
525 data.len() as u64,
526 ));
527
528 Ok(())
529 }
530
531 pub fn read_vector_index(&self, field_id: FieldId) -> Result<Option<Vec<u8>>> {
534 let entry = match self
535 .committed
536 .vector_indexes
537 .iter()
538 .find(|e| e.field_id == field_id)
539 {
540 Some(e) => e,
541 None => return Ok(None),
542 };
543
544 let mut buf = vec![0u8; entry.data_len as usize];
545 self.file
546 .read_exact_at(&mut buf, entry.extent.start.byte_offset())?;
547 Ok(Some(buf))
548 }
549
550 pub fn vector_index_fields(&self) -> Vec<FieldId> {
552 self.committed
553 .vector_indexes
554 .iter()
555 .map(|e| e.field_id)
556 .collect()
557 }
558
559 pub fn remove_vector_index(&mut self, field_id: FieldId) {
561 self.pending_vector_index_removals.push(field_id);
562 }
563}
564
565#[cfg(unix)]
566impl crate::storage::Storage for SingleFileDirectory {
567 fn write_segment(&mut self, segment_id: SegmentId, data: &[u8]) -> Result<()> {
568 self.write_segment(segment_id, data)
569 }
570 fn read_segment(&self, segment_id: SegmentId) -> Result<Vec<u8>> {
571 self.read_segment(segment_id)
572 }
573 fn commit(&mut self) -> Result<()> {
574 self.commit()
575 }
576 fn segments(&self) -> &[SegmentEntry] {
577 self.segments()
578 }
579 fn generation(&self) -> u64 {
580 self.generation()
581 }
582 fn set_user_metadata(&mut self, metadata: Vec<u8>) {
583 self.set_user_metadata(metadata)
584 }
585 fn user_metadata(&self) -> &[u8] {
586 self.user_metadata()
587 }
588 fn remove_segments(&mut self, segment_ids: &[SegmentId]) {
589 self.remove_segments(segment_ids)
590 }
591 fn write_vector_index(&mut self, field_id: FieldId, data: &[u8]) -> Result<()> {
592 self.write_vector_index(field_id, data)
593 }
594 fn read_vector_index(&self, field_id: FieldId) -> Result<Option<Vec<u8>>> {
595 self.read_vector_index(field_id)
596 }
597 fn vector_index_fields(&self) -> Vec<FieldId> {
598 self.vector_index_fields()
599 }
600 fn remove_vector_index(&mut self, field_id: FieldId) {
601 self.remove_vector_index(field_id)
602 }
603 fn set_write_timeout(&mut self, timeout: std::time::Duration) {
604 self.set_write_timeout(timeout)
605 }
606}
607
608#[cfg(unix)]
613fn load_metadata(file: &File, header: &FileHeader) -> Result<(MetadataSnapshot, bool)> {
614 if let Some(snap) = try_load_root(file, header.active_root_pointer())? {
616 return Ok((snap, false));
617 }
618
619 if let Some(snap) = try_load_root(file, header.inactive_root_pointer())? {
621 return Ok((snap, true));
622 }
623
624 if !header.active_root_pointer().is_populated()
626 && !header.inactive_root_pointer().is_populated()
627 {
628 return Ok((MetadataSnapshot::empty(), false));
629 }
630
631 Err(LuciError::IndexCorrupted(
633 "both root pointers failed checksum validation".into(),
634 ))
635}
636
637#[cfg(unix)]
641fn try_load_root(file: &File, root: &RootPointer) -> Result<Option<MetadataSnapshot>> {
642 let block_id = match root.block_id {
643 Some(id) => id,
644 None => return Ok(None),
645 };
646
647 let mut block_buf = vec![0u8; BLOCK_SIZE as usize];
648 file.read_exact_at(&mut block_buf, block_id.byte_offset())?;
649
650 let computed = xxh3_checksum(&block_buf);
651 if computed != root.checksum {
652 return Ok(None);
653 }
654
655 let snap = MetadataSnapshot::from_bytes(&block_buf)?;
656 Ok(Some(snap))
657}
658
659#[cfg(test)]
660#[cfg(unix)]
661mod tests {
662 use super::*;
663 use crate::storage::header::FORMAT_VERSION;
664 use std::fs;
665
666 fn test_dir() -> std::path::PathBuf {
668 let dir = std::env::temp_dir().join(format!("luci_test_{}", std::process::id()));
669 fs::create_dir_all(&dir).unwrap();
670 dir
671 }
672
673 fn test_path(name: &str) -> std::path::PathBuf {
674 test_dir().join(name)
675 }
676
677 #[test]
678 fn create_new_file() {
679 let path = test_path("create_new.luci");
680 let _ = fs::remove_file(&path);
681
682 let dir = SingleFileDirectory::create(&path).unwrap();
683 assert!(dir.segments().is_empty());
684 assert_eq!(dir.generation(), 0);
685
686 let meta = fs::metadata(&path).unwrap();
688 assert!(meta.len() >= HEADER_SIZE as u64);
689
690 fs::remove_file(&path).unwrap();
691 }
692
693 #[test]
694 fn create_fails_if_exists() {
695 let path = test_path("create_exists.luci");
696 let _ = fs::remove_file(&path);
697
698 let _dir = SingleFileDirectory::create(&path).unwrap();
699 let err = SingleFileDirectory::create(&path);
700 assert!(err.is_err());
701
702 fs::remove_file(&path).unwrap();
703 }
704
705 #[test]
706 fn write_commit_read() {
707 let path = test_path("write_commit_read.luci");
708 let _ = fs::remove_file(&path);
709
710 let mut dir = SingleFileDirectory::create(&path).unwrap();
711
712 let data = b"hello luci segment data!";
713 dir.write_segment(SegmentId::new(1), data).unwrap();
714 dir.commit().unwrap();
715
716 let read_back = dir.read_segment(SegmentId::new(1)).unwrap();
717 assert_eq!(read_back, data);
718 assert_eq!(dir.segments().len(), 1);
719 assert_eq!(dir.generation(), 1);
720
721 fs::remove_file(&path).unwrap();
722 }
723
724 #[test]
725 fn multiple_segments() {
726 let path = test_path("multiple_segments.luci");
727 let _ = fs::remove_file(&path);
728
729 let mut dir = SingleFileDirectory::create(&path).unwrap();
730
731 dir.write_segment(SegmentId::new(1), b"segment-one")
732 .unwrap();
733 dir.write_segment(SegmentId::new(2), b"segment-two-longer")
734 .unwrap();
735 dir.commit().unwrap();
736
737 assert_eq!(dir.segments().len(), 2);
738 assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"segment-one");
739 assert_eq!(
740 dir.read_segment(SegmentId::new(2)).unwrap(),
741 b"segment-two-longer"
742 );
743
744 fs::remove_file(&path).unwrap();
745 }
746
747 #[test]
748 fn reopen_after_commit() {
749 let path = test_path("reopen.luci");
750 let _ = fs::remove_file(&path);
751
752 {
754 let mut dir = SingleFileDirectory::create(&path).unwrap();
755 dir.write_segment(SegmentId::new(1), b"persistent data")
756 .unwrap();
757 dir.commit().unwrap();
758 }
759
760 {
762 let dir = SingleFileDirectory::open(&path).unwrap();
763 assert_eq!(dir.segments().len(), 1);
764 assert_eq!(
765 dir.read_segment(SegmentId::new(1)).unwrap(),
766 b"persistent data"
767 );
768 assert_eq!(dir.generation(), 1);
769 }
770
771 fs::remove_file(&path).unwrap();
772 }
773
774 #[test]
775 fn uncommitted_writes_not_visible_after_reopen() {
776 let path = test_path("uncommitted.luci");
777 let _ = fs::remove_file(&path);
778
779 {
780 let mut dir = SingleFileDirectory::create(&path).unwrap();
781 dir.write_segment(SegmentId::new(1), b"committed").unwrap();
782 dir.commit().unwrap();
783
784 dir.write_segment(SegmentId::new(2), b"uncommitted")
786 .unwrap();
787 }
788
789 {
790 let dir = SingleFileDirectory::open(&path).unwrap();
791 assert_eq!(dir.segments().len(), 1);
792 assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"committed");
793 assert!(dir.read_segment(SegmentId::new(2)).is_err());
794 }
795
796 fs::remove_file(&path).unwrap();
797 }
798
799 #[test]
800 fn multiple_commits() {
801 let path = test_path("multi_commit.luci");
802 let _ = fs::remove_file(&path);
803
804 let mut dir = SingleFileDirectory::create(&path).unwrap();
805
806 dir.write_segment(SegmentId::new(1), b"first").unwrap();
807 dir.commit().unwrap();
808 assert_eq!(dir.generation(), 1);
809
810 dir.write_segment(SegmentId::new(2), b"second").unwrap();
811 dir.commit().unwrap();
812 assert_eq!(dir.generation(), 2);
813
814 dir.write_segment(SegmentId::new(3), b"third").unwrap();
815 dir.commit().unwrap();
816 assert_eq!(dir.generation(), 3);
817
818 assert_eq!(dir.segments().len(), 3);
819 assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"first");
820 assert_eq!(dir.read_segment(SegmentId::new(2)).unwrap(), b"second");
821 assert_eq!(dir.read_segment(SegmentId::new(3)).unwrap(), b"third");
822
823 fs::remove_file(&path).unwrap();
824 }
825
826 #[test]
827 fn reopen_after_multiple_commits() {
828 let path = test_path("reopen_multi.luci");
829 let _ = fs::remove_file(&path);
830
831 {
832 let mut dir = SingleFileDirectory::create(&path).unwrap();
833 dir.write_segment(SegmentId::new(1), b"aaa").unwrap();
834 dir.commit().unwrap();
835 dir.write_segment(SegmentId::new(2), b"bbb").unwrap();
836 dir.commit().unwrap();
837 dir.write_segment(SegmentId::new(3), b"ccc").unwrap();
838 dir.commit().unwrap();
839 }
840
841 {
842 let dir = SingleFileDirectory::open(&path).unwrap();
843 assert_eq!(dir.segments().len(), 3);
844 assert_eq!(dir.read_segment(SegmentId::new(1)).unwrap(), b"aaa");
845 assert_eq!(dir.read_segment(SegmentId::new(2)).unwrap(), b"bbb");
846 assert_eq!(dir.read_segment(SegmentId::new(3)).unwrap(), b"ccc");
847 assert_eq!(dir.generation(), 3);
848 }
849
850 fs::remove_file(&path).unwrap();
851 }
852
853 #[test]
854 fn large_segment_spanning_multiple_blocks() {
855 let path = test_path("large_segment.luci");
856 let _ = fs::remove_file(&path);
857
858 let mut dir = SingleFileDirectory::create(&path).unwrap();
859
860 let data = vec![0xABu8; BLOCK_SIZE as usize * 3 + 1000];
862 dir.write_segment(SegmentId::new(1), &data).unwrap();
863 dir.commit().unwrap();
864
865 let read_back = dir.read_segment(SegmentId::new(1)).unwrap();
866 assert_eq!(read_back, data);
867
868 assert_eq!(dir.segments()[0].extent.count, 4);
870
871 fs::remove_file(&path).unwrap();
872 }
873
874 #[test]
875 fn read_nonexistent_segment_is_error() {
876 let path = test_path("read_nonexist.luci");
877 let _ = fs::remove_file(&path);
878
879 let dir = SingleFileDirectory::create(&path).unwrap();
880 let err = dir.read_segment(SegmentId::new(999));
881 assert!(err.is_err());
882
883 fs::remove_file(&path).unwrap();
884 }
885
886 #[test]
887 fn empty_commit_is_noop() {
888 let path = test_path("empty_commit.luci");
889 let _ = fs::remove_file(&path);
890
891 let mut dir = SingleFileDirectory::create(&path).unwrap();
892 dir.commit().unwrap();
894 assert_eq!(dir.generation(), 0);
895 assert!(dir.segments().is_empty());
896
897 fs::remove_file(&path).unwrap();
898 }
899
900 #[test]
901 fn simulated_torn_header_falls_back() {
902 let path = test_path("torn_header.luci");
903 let _ = fs::remove_file(&path);
904
905 {
907 let mut dir = SingleFileDirectory::create(&path).unwrap();
908 dir.write_segment(SegmentId::new(1), b"first-commit")
909 .unwrap();
910 dir.commit().unwrap();
911 dir.write_segment(SegmentId::new(2), b"second-commit")
912 .unwrap();
913 dir.commit().unwrap();
914 }
915
916 {
919 let file = OpenOptions::new()
920 .read(true)
921 .write(true)
922 .open(&path)
923 .unwrap();
924 let mut header_buf = [0u8; HEADER_SIZE as usize];
925 file.read_exact_at(&mut header_buf, 0).unwrap();
926 let header = FileHeader::from_bytes(&header_buf).unwrap();
927
928 let checksum_offset = match header.active_root {
930 crate::storage::ActiveRoot::A => 24usize, crate::storage::ActiveRoot::B => 40usize, };
933 header_buf[checksum_offset] ^= 0xFF;
934 file.write_all_at(&header_buf, 0).unwrap();
935 file.sync_all().unwrap();
936 }
937
938 {
940 let dir = SingleFileDirectory::open(&path).unwrap();
941 assert_eq!(dir.segments().len(), 1);
942 assert_eq!(
943 dir.read_segment(SegmentId::new(1)).unwrap(),
944 b"first-commit"
945 );
946 assert!(dir.read_segment(SegmentId::new(2)).is_err());
948 }
949
950 fs::remove_file(&path).unwrap();
951 }
952
953 #[test]
960 fn version_stamped_on_any_commit() {
961 let path = test_path("version_stamp.luci");
962 let _ = fs::remove_file(&path);
963
964 SingleFileDirectory::create(&path).unwrap();
967 {
968 let file = OpenOptions::new()
969 .read(true)
970 .write(true)
971 .open(&path)
972 .unwrap();
973 let v2 = FileHeader::with_format_version(2).to_bytes();
974 file.write_all_at(&v2, 0).unwrap();
975 file.sync_all().unwrap();
976 }
977 {
979 let file = OpenOptions::new().read(true).open(&path).unwrap();
980 let mut buf = [0u8; HEADER_SIZE as usize];
981 file.read_exact_at(&mut buf, 0).unwrap();
982 assert_eq!(FileHeader::from_bytes(&buf).unwrap().format_version, 2);
983 }
984
985 {
989 let mut dir = SingleFileDirectory::open(&path).unwrap();
990 dir.set_user_metadata(b"deletion-marker".to_vec());
991 dir.commit().unwrap();
992 }
993
994 {
998 let file = OpenOptions::new().read(true).open(&path).unwrap();
999 let mut buf = [0u8; HEADER_SIZE as usize];
1000 file.read_exact_at(&mut buf, 0).unwrap();
1001 assert_eq!(
1002 FileHeader::from_bytes(&buf).unwrap().format_version,
1003 FORMAT_VERSION
1004 );
1005 }
1006
1007 fs::remove_file(&path).unwrap();
1008 }
1009}