1use anyhow::{anyhow, Context, Result};
8use crc32fast::Hasher as Crc32Hasher;
9use std::collections::BTreeMap;
10use std::fs::File;
11use std::io::{Read, Seek, SeekFrom, Write};
12use std::path::Path;
13
14#[cfg(feature = "debug-prints")]
16macro_rules! debug_print {
17 ($($arg:tt)*) => {
18 eprintln!($($arg)*);
19 };
20}
21
22#[cfg(not(feature = "debug-prints"))]
23macro_rules! debug_print {
24 ($($arg:tt)*) => {
25 ()
26 };
27}
28
29pub const FILE_MAGIC: [u8; 8] = *b"GEODB\0\0\0";
35
36pub const FORMAT_VERSION: u32 = 1;
38
39pub const HEADER_SIZE: u64 = 128;
41pub const HEADER_SIZE_USIZE: usize = 128;
42
43pub const SECTION_ENTRY_SIZE: u64 = 64;
45pub const SECTION_ENTRY_SIZE_USIZE: usize = 64;
46
47pub const MAX_SECTION_NAME_LEN: usize = 32;
49
50#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct GeoFileHeader {
72 pub magic: [u8; 8],
73 pub version: u32,
74 pub flags: u32,
75 pub section_table_offset: u64,
76 pub section_count: u64,
77 pub next_data_offset: u64,
78 pub created_at_epoch: u64,
79 pub modified_at_epoch: u64,
80 pub reserved: [u8; 72],
81}
82
83impl Default for GeoFileHeader {
84 fn default() -> Self {
85 let now = std::time::SystemTime::now()
86 .duration_since(std::time::UNIX_EPOCH)
87 .unwrap_or_default()
88 .as_secs();
89
90 Self {
91 magic: FILE_MAGIC,
92 version: FORMAT_VERSION,
93 flags: 0,
94 section_table_offset: HEADER_SIZE,
95 section_count: 0,
96 next_data_offset: HEADER_SIZE,
97 created_at_epoch: now,
98 modified_at_epoch: 0,
99 reserved: [0u8; 72],
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct SectionEntry {
124 pub name: String,
125 pub offset: u64,
126 pub length: u64,
127 pub capacity: u64,
128 pub flags: u32,
129 pub checksum: u32,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Section {
139 pub name: String,
140 pub offset: u64,
141 pub length: u64,
142 pub capacity: u64,
143 pub flags: u32,
144 pub checksum: u32,
145}
146
147impl std::fmt::Debug for SectionedStorage {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 f.debug_struct("SectionedStorage")
163 .field("path", &self.path)
164 .field("header", &self.header)
165 .field("section_count", &self.sections.len())
166 .field("dirty", &self.dirty)
167 .finish()
168 }
169}
170
171pub struct SectionedStorage {
172 file: File,
173 path: std::path::PathBuf,
174 header: GeoFileHeader,
175 sections: BTreeMap<String, Section>,
176 dirty: bool,
177}
178
179pub fn encode_header(header: &GeoFileHeader) -> [u8; HEADER_SIZE_USIZE] {
184 let mut buf = [0u8; HEADER_SIZE_USIZE];
185
186 buf[0..8].copy_from_slice(&header.magic);
188
189 buf[8..12].copy_from_slice(&header.version.to_le_bytes());
191
192 buf[12..16].copy_from_slice(&header.flags.to_le_bytes());
194
195 buf[16..24].copy_from_slice(&header.section_table_offset.to_le_bytes());
197
198 buf[24..32].copy_from_slice(&header.section_count.to_le_bytes());
200
201 buf[32..40].copy_from_slice(&header.next_data_offset.to_le_bytes());
203
204 buf[40..48].copy_from_slice(&header.created_at_epoch.to_le_bytes());
206
207 buf[48..56].copy_from_slice(&header.modified_at_epoch.to_le_bytes());
209
210 buf
213}
214
215pub fn decode_header(buf: &[u8; HEADER_SIZE_USIZE]) -> Result<GeoFileHeader> {
216 let magic: [u8; 8] = buf[0..8]
218 .try_into()
219 .map_err(|_| anyhow!("Magic slice has wrong size"))?;
220
221 if magic != FILE_MAGIC {
222 return Err(anyhow!(
223 "Invalid magic: expected {:?}, got {:?}",
224 FILE_MAGIC,
225 magic
226 ));
227 }
228
229 let version = u32::from_le_bytes(buf[8..12].try_into()?);
230 if version != FORMAT_VERSION {
231 return Err(anyhow!(
232 "Unsupported version: expected {}, got {}",
233 FORMAT_VERSION,
234 version
235 ));
236 }
237
238 Ok(GeoFileHeader {
239 magic,
240 version,
241 flags: u32::from_le_bytes(buf[12..16].try_into()?),
242 section_table_offset: u64::from_le_bytes(buf[16..24].try_into()?),
243 section_count: u64::from_le_bytes(buf[24..32].try_into()?),
244 next_data_offset: u64::from_le_bytes(buf[32..40].try_into()?),
245 created_at_epoch: u64::from_le_bytes(buf[40..48].try_into()?),
246 modified_at_epoch: u64::from_le_bytes(buf[48..56].try_into()?),
247 reserved: {
248 let mut arr = [0u8; 72];
249 arr.copy_from_slice(&buf[56..128]);
250 arr
251 },
252 })
253}
254
255fn encode_section_entry_name(name: &str) -> [u8; MAX_SECTION_NAME_LEN] {
256 let mut buf = [0u8; MAX_SECTION_NAME_LEN];
257 let name_bytes = name.as_bytes();
258 let len = name_bytes.len().min(MAX_SECTION_NAME_LEN);
259 buf[..len].copy_from_slice(&name_bytes[..len]);
260 buf
261}
262
263fn decode_section_entry_name(buf: &[u8]) -> Result<String> {
264 let len = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
265 String::from_utf8(buf[..len].to_vec()).context("Section name is not valid UTF-8")
266}
267
268pub fn encode_section_entry(entry: &SectionEntry) -> [u8; SECTION_ENTRY_SIZE_USIZE] {
269 let mut buf = [0u8; SECTION_ENTRY_SIZE_USIZE];
270
271 buf[0..32].copy_from_slice(&encode_section_entry_name(&entry.name));
273
274 buf[32..40].copy_from_slice(&entry.offset.to_le_bytes());
276
277 buf[40..48].copy_from_slice(&entry.length.to_le_bytes());
279
280 buf[48..56].copy_from_slice(&entry.capacity.to_le_bytes());
282
283 buf[56..60].copy_from_slice(&entry.flags.to_le_bytes());
285
286 buf[60..64].copy_from_slice(&entry.checksum.to_le_bytes());
288
289 buf
292}
293
294pub fn decode_section_entry(buf: &[u8; SECTION_ENTRY_SIZE_USIZE]) -> Result<SectionEntry> {
295 let name = decode_section_entry_name(&buf[0..32])?;
296
297 Ok(SectionEntry {
298 name,
299 offset: u64::from_le_bytes(buf[32..40].try_into()?),
300 length: u64::from_le_bytes(buf[40..48].try_into()?),
301 capacity: u64::from_le_bytes(buf[48..56].try_into()?),
302 flags: u32::from_le_bytes(buf[56..60].try_into()?),
303 checksum: u32::from_le_bytes(buf[60..64].try_into()?),
304 })
305}
306
307pub fn compute_checksum(data: &[u8]) -> u32 {
308 let mut hasher = Crc32Hasher::new();
309 hasher.update(data);
310 hasher.finalize()
311}
312
313impl SectionedStorage {
318 pub fn create(path: &Path) -> Result<Self> {
320 let mut file = std::fs::OpenOptions::new()
321 .read(true)
322 .write(true)
323 .create(true)
324 .truncate(true)
325 .open(path)
326 .context("Failed to create sectioned file")?;
327
328 let header = GeoFileHeader::default();
329 let header_bytes = encode_header(&header);
330 file.write_all(&header_bytes)
331 .context("Failed to write header")?;
332 file.sync_all().context("Failed to sync file")?;
333
334 Ok(Self {
335 file,
336 path: path.to_path_buf(),
337 header,
338 sections: BTreeMap::new(),
339 dirty: false,
340 })
341 }
342
343 pub fn is_sectioned_file(path: &Path) -> bool {
348 if !path.exists() {
349 return false;
350 }
351
352 match std::fs::File::open(path) {
354 Ok(mut file) => {
355 let mut header_buf = [0u8; HEADER_SIZE_USIZE];
356 if file.read_exact(&mut header_buf).is_err() {
357 return false;
358 }
359 match decode_header(&header_buf) {
361 Ok(header) => header.magic == FILE_MAGIC,
362 Err(_) => false,
363 }
364 }
365 Err(_) => false,
366 }
367 }
368
369 pub fn open(path: &Path) -> Result<Self> {
374 let mut file = std::fs::OpenOptions::new()
375 .read(true)
376 .write(true)
377 .open(path)
378 .context("Failed to open sectioned file")?;
379
380 let mut header_buf = [0u8; HEADER_SIZE_USIZE];
382 file.read_exact(&mut header_buf)
383 .context("Failed to read header")?;
384 let header = decode_header(&header_buf)?;
385
386 let mut sections = BTreeMap::new();
388
389 if header.section_count > 0 {
390 file.seek(SeekFrom::Start(header.section_table_offset))
391 .context("Failed to seek to section table")?;
392
393 for _ in 0..header.section_count {
394 let mut entry_buf = [0u8; SECTION_ENTRY_SIZE_USIZE];
395 file.read_exact(&mut entry_buf)
396 .context("Failed to read section entry")?;
397 let entry = decode_section_entry(&entry_buf)?;
398
399 sections.insert(
400 entry.name.clone(),
401 Section {
402 name: entry.name,
403 offset: entry.offset,
404 length: entry.length,
405 capacity: entry.capacity,
406 flags: entry.flags,
407 checksum: entry.checksum,
408 },
409 );
410 }
411 }
412
413 let mut storage = Self {
414 file,
415 path: path.to_path_buf(),
416 header,
417 sections,
418 dirty: false,
419 };
420
421 storage.validate()?;
423
424 Ok(storage)
425 }
426
427 pub fn create_section(&mut self, name: &str, capacity: u64, flags: u32) -> Result<()> {
444 if name.is_empty() {
446 return Err(anyhow!("Section name cannot be empty"));
447 }
448
449 let name_bytes = name.as_bytes();
451 if name_bytes.len() > MAX_SECTION_NAME_LEN {
452 return Err(anyhow!(
453 "Section name too long: {} bytes > {}",
454 name_bytes.len(),
455 MAX_SECTION_NAME_LEN
456 ));
457 }
458
459 if self.sections.contains_key(name) {
461 return Err(anyhow!("Section '{}' already exists", name));
462 }
463
464 let file_len = self
466 .file
467 .metadata()
468 .context("Failed to get file metadata")?
469 .len();
470
471 let allocation_base = self.header.next_data_offset.max(file_len);
473
474 let new_next_data_offset = allocation_base.checked_add(capacity).ok_or_else(|| {
476 anyhow!(
477 "Data offset overflow: allocation_base {} + capacity {}",
478 allocation_base,
479 capacity
480 )
481 })?;
482
483 self.file.set_len(new_next_data_offset).with_context(|| {
485 format!(
486 "Failed to reserve {} bytes for section '{}' at offset {}",
487 capacity, name, allocation_base
488 )
489 })?;
490
491 let offset = allocation_base;
493
494 self.header.next_data_offset = new_next_data_offset;
496
497 self.sections.insert(
499 name.to_string(),
500 Section {
501 name: name.to_string(),
502 offset,
503 length: 0,
504 capacity,
505 flags,
506 checksum: 0,
507 },
508 );
509
510 self.dirty = true;
511 Ok(())
512 }
513
514 pub fn write_section(&mut self, name: &str, data: &[u8]) -> Result<()> {
523 debug_print!(
524 "[WRITE_SECTION] Writing section '{}': {} bytes",
525 name,
526 data.len()
527 );
528 let section = self
529 .sections
530 .get(name)
531 .ok_or_else(|| anyhow!("Section '{}' not found", name))?;
532
533 if data.len() as u64 > section.capacity {
534 return Err(anyhow!(
536 "Section '{}' overflow: attempted to write {} bytes, but capacity is {} bytes ({} bytes over limit)\n\
537 Section details: offset={}, length={}, capacity={}\n\
538 To fix: Increase section capacity during database creation or migrate to a larger capacity.",
539 name,
540 data.len(),
541 section.capacity,
542 data.len() as u64 - section.capacity,
543 section.offset,
544 section.length,
545 section.capacity
546 ));
547 }
548
549 let checksum = compute_checksum(data);
550
551 self.file
552 .seek(SeekFrom::Start(section.offset))
553 .context("Failed to seek to section")?;
554 self.file
555 .write_all(data)
556 .context("Failed to write section data")?;
557
558 if let Some(s) = self.sections.get_mut(name) {
560 s.length = data.len() as u64;
561 s.checksum = checksum;
562 debug_print!(
563 "[WRITE_SECTION] Updated section '{}' metadata: length={}, checksum={}",
564 name,
565 s.length,
566 s.checksum
567 );
568 }
569
570 self.dirty = true;
571 Ok(())
572 }
573
574 pub fn read_section(&mut self, name: &str) -> Result<Vec<u8>> {
580 let section = self
581 .sections
582 .get(name)
583 .ok_or_else(|| anyhow!("Section '{}' not found", name))?;
584
585 if section.length == 0 {
586 return Ok(Vec::new());
587 }
588
589 self.file
590 .seek(SeekFrom::Start(section.offset))
591 .context("Failed to seek to section")?;
592
593 let mut buffer = vec![0u8; section.length as usize];
594 self.file
595 .read_exact(&mut buffer)
596 .context("Failed to read section data")?;
597
598 let computed = compute_checksum(&buffer);
600 if computed != section.checksum {
601 return Err(anyhow!(
602 "Checksum mismatch for section '{}': stored {}, computed {}",
603 name,
604 section.checksum,
605 computed
606 ));
607 }
608
609 Ok(buffer)
610 }
611
612 pub fn get_section(&self, name: &str) -> Option<&Section> {
614 self.sections.get(name)
615 }
616
617 pub fn list_sections(&self) -> Vec<Section> {
619 self.sections.values().cloned().collect()
620 }
621
622 pub fn section_count(&self) -> usize {
624 self.sections.len()
625 }
626
627 pub fn path(&self) -> &Path {
629 &self.path
630 }
631
632 pub fn header(&self) -> &GeoFileHeader {
634 &self.header
635 }
636
637 pub fn flush(&mut self) -> Result<()> {
657 let now = std::time::SystemTime::now()
658 .duration_since(std::time::UNIX_EPOCH)
659 .unwrap_or_default()
660 .as_secs();
661
662 let table_offset = self
664 .file
665 .seek(SeekFrom::End(0))
666 .context("Failed to seek to EOF for section table")?;
667
668 debug_print!(
669 "[FLUSH_DEBUG] Writing section table at offset {}",
670 table_offset
671 );
672 debug_print!("[FLUSH_DEBUG] Section count: {}", self.sections.len());
673
674 for section in self.sections.values() {
676 debug_print!(
677 "[FLUSH_DEBUG] Section {}: offset={}, length={}",
678 section.name,
679 section.offset,
680 section.length
681 );
682 let entry = SectionEntry {
683 name: section.name.clone(),
684 offset: section.offset,
685 length: section.length,
686 capacity: section.capacity,
687 flags: section.flags,
688 checksum: section.checksum,
689 };
690 let entry_bytes = encode_section_entry(&entry);
691 self.file
692 .write_all(&entry_bytes)
693 .context("Failed to write section entry")?;
694 }
695
696 self.header.section_table_offset = table_offset;
698 self.header.section_count = self.sections.len() as u64;
699 self.header.modified_at_epoch = now;
700
701 self.file
703 .seek(SeekFrom::Start(0))
704 .context("Failed to seek to header")?;
705 let header_bytes = encode_header(&self.header);
706 self.file
707 .write_all(&header_bytes)
708 .context("Failed to write header")?;
709
710 self.file.sync_all().context("Failed to sync file")?;
711
712 self.dirty = false;
713 Ok(())
714 }
715
716 pub fn validate(&mut self) -> Result<()> {
721 if self.dirty {
723 return Err(anyhow!(
724 "cannot validate dirty/unflushed state; flush first"
725 ));
726 }
727
728 let metadata = self
730 .file
731 .metadata()
732 .context("Failed to get file metadata")?;
733 let file_len = metadata.len();
734
735 if file_len < HEADER_SIZE {
737 return Err(anyhow!(
738 "File too small: {} < header size {}",
739 file_len,
740 HEADER_SIZE
741 ));
742 }
743
744 let mut header_buf = [0u8; HEADER_SIZE_USIZE];
746 self.file
747 .seek(SeekFrom::Start(0))
748 .context("Failed to seek to header for validation")?;
749 self.file
750 .read_exact(&mut header_buf)
751 .context("Failed to read header for validation")?;
752 let disk_header = decode_header(&header_buf)?;
753
754 self.header = disk_header.clone();
756
757 if disk_header.section_table_offset < HEADER_SIZE {
759 return Err(anyhow!(
760 "Section table offset {} before data area {}",
761 disk_header.section_table_offset,
762 HEADER_SIZE
763 ));
764 }
765 if disk_header.section_table_offset > file_len {
766 return Err(anyhow!(
767 "Section table offset {} beyond file length {}",
768 disk_header.section_table_offset,
769 file_len
770 ));
771 }
772
773 if file_len < disk_header.next_data_offset {
775 return Err(anyhow!(
776 "File truncated: length {} < next_data_offset {} (physical reservation missing)",
777 file_len,
778 disk_header.next_data_offset
779 ));
780 }
781
782 if disk_header.next_data_offset > disk_header.section_table_offset {
784 return Err(anyhow!(
785 "Data area overlaps table: next_data_offset {} > section_table_offset {}",
786 disk_header.next_data_offset,
787 disk_header.section_table_offset
788 ));
789 }
790
791 if disk_header.section_count > 0 {
793 let table_size = disk_header
794 .section_count
795 .checked_mul(SECTION_ENTRY_SIZE)
796 .ok_or_else(|| anyhow!("Section count overflow"))?;
797 let table_end = disk_header
798 .section_table_offset
799 .checked_add(table_size)
800 .ok_or_else(|| anyhow!("Section table end overflow"))?;
801 if table_end > file_len {
802 return Err(anyhow!(
803 "Section table extends beyond file: offset {} + size {} = {} > length {}",
804 disk_header.section_table_offset,
805 table_size,
806 table_end,
807 file_len
808 ));
809 }
810 }
811
812 for name in self.sections.keys() {
814 if name.is_empty() {
815 return Err(anyhow!("Section name is empty"));
816 }
817 }
818
819 let mut prev_end = HEADER_SIZE;
821 let mut sorted_sections: Vec<_> = self.sections.iter().collect();
822 sorted_sections.sort_by_key(|(_, section)| section.offset);
823 for (name, section) in sorted_sections {
824 if section.offset < HEADER_SIZE {
826 return Err(anyhow!(
827 "Section '{}' offset {} before data area {}",
828 name,
829 section.offset,
830 HEADER_SIZE
831 ));
832 }
833 if section.offset >= disk_header.section_table_offset {
834 return Err(anyhow!(
835 "Section '{}' offset {} at or after current table {}",
836 name,
837 section.offset,
838 disk_header.section_table_offset
839 ));
840 }
841
842 if section.capacity < section.length {
844 return Err(anyhow!(
845 "Section '{}' capacity {} < length {}",
846 name,
847 section.capacity,
848 section.length
849 ));
850 }
851
852 let section_end = section
854 .offset
855 .checked_add(section.capacity)
856 .ok_or_else(|| anyhow!("Section '{}' end overflow", name))?;
857 if section_end > disk_header.section_table_offset {
858 return Err(anyhow!(
859 "Section '{}' (offset {} + capacity {} = {}) extends beyond current table start {}",
860 name,
861 section.offset,
862 section.capacity,
863 section_end,
864 disk_header.section_table_offset
865 ));
866 }
867
868 if section.offset < prev_end {
870 return Err(anyhow!(
871 "Section '{}' (offset {}) overlaps previous section (ends at {})",
872 name,
873 section.offset,
874 prev_end
875 ));
876 }
877 prev_end = section_end;
878
879 if section.length > 0 {
881 self.file
882 .seek(SeekFrom::Start(section.offset))
883 .context("Failed to seek to section for checksum validation")?;
884 let mut buf = vec![0u8; section.length as usize];
885 self.file
886 .read_exact(&mut buf)
887 .context("Failed to read section for checksum validation")?;
888 let computed = compute_checksum(&buf);
889 if computed != section.checksum {
890 return Err(anyhow!(
891 "Checksum mismatch for section '{}': stored {}, computed {}",
892 name,
893 section.checksum,
894 computed
895 ));
896 }
897 }
898 }
899
900 if disk_header.next_data_offset < prev_end {
902 return Err(anyhow!(
903 "next_data_offset {} overlaps last section (ends at {})",
904 disk_header.next_data_offset,
905 prev_end
906 ));
907 }
908
909 if disk_header.section_count == 0 && disk_header.section_table_offset != HEADER_SIZE {
911 return Err(anyhow!(
912 "Empty file: section_table_offset should be {}, got {}",
913 HEADER_SIZE,
914 disk_header.section_table_offset
915 ));
916 }
917
918 Ok(())
919 }
920
921 pub fn validate_required_sections(&self, required: &[&str]) -> Result<()> {
923 for name in required {
924 if !self.sections.contains_key(*name) {
925 return Err(anyhow!("Required section '{}' is missing", name));
926 }
927 }
928 Ok(())
929 }
930
931 pub fn resize_section(&mut self, name: &str, new_capacity: u64) -> Result<()> {
948 let (offset, length, _capacity, flags, checksum) = {
950 let section = self
951 .sections
952 .get(name)
953 .ok_or_else(|| anyhow!("Section '{}' not found", name))?;
954
955 if new_capacity < section.length {
956 return Err(anyhow!(
957 "Cannot resize section '{}' to {} bytes: current data length is {} bytes",
958 name,
959 new_capacity,
960 section.length
961 ));
962 }
963
964 if new_capacity == section.capacity {
965 return Ok(());
967 }
968
969 debug_print!(
970 "[RESIZE_SECTION] Resizing '{}' from {} to {} bytes",
971 name,
972 section.capacity,
973 new_capacity
974 );
975
976 (
977 section.offset,
978 section.length,
979 section.capacity,
980 section.flags,
981 section.checksum,
982 )
983 };
984
985 let current_data = if length > 0 {
987 self.file
988 .seek(SeekFrom::Start(offset))
989 .context("Failed to seek to section for resize")?;
990 let mut buffer = vec![0u8; length as usize];
991 self.file
992 .read_exact(&mut buffer)
993 .context("Failed to read section data for resize")?;
994 buffer
995 } else {
996 Vec::new()
997 };
998
999 let file_len = self
1001 .file
1002 .metadata()
1003 .context("Failed to get file metadata")?
1004 .len();
1005 let allocation_base = self.header.next_data_offset.max(file_len);
1006
1007 let new_next_data_offset = allocation_base
1009 .checked_add(new_capacity)
1010 .ok_or_else(|| anyhow!("Data offset overflow during section resize"))?;
1011
1012 self.file
1014 .set_len(new_next_data_offset)
1015 .context("Failed to extend file for section resize")?;
1016
1017 let new_offset = allocation_base;
1019
1020 if !current_data.is_empty() {
1022 self.file
1023 .seek(SeekFrom::Start(new_offset))
1024 .context("Failed to seek to new section location")?;
1025 self.file
1026 .write_all(¤t_data)
1027 .context("Failed to write data to new section location")?;
1028 }
1029
1030 self.sections.remove(name);
1032 self.sections.insert(
1033 name.to_string(),
1034 Section {
1035 name: name.to_string(),
1036 offset: new_offset,
1037 length,
1038 capacity: new_capacity,
1039 flags,
1040 checksum,
1041 },
1042 );
1043
1044 self.header.next_data_offset = new_next_data_offset;
1046 self.dirty = true;
1047
1048 debug_print!(
1049 "[RESIZE_SECTION] Section '{}' moved from offset {} to {}",
1050 name,
1051 offset,
1052 new_offset
1053 );
1054
1055 Ok(())
1056 }
1057}
1058
1059#[cfg(test)]
1064mod tests {
1065 use super::*;
1066
1067 #[test]
1070 fn test_header_exactly_128_bytes() {
1071 let header = GeoFileHeader::default();
1072 let encoded = encode_header(&header);
1073 assert_eq!(encoded.len(), 128, "Header must be exactly 128 bytes");
1074 }
1075
1076 #[test]
1077 fn test_header_reserved_field_size() {
1078 let header = GeoFileHeader::default();
1079 assert_eq!(header.reserved.len(), 72);
1080 }
1081
1082 #[test]
1083 fn test_header_roundtrip_preserves_all_fields() {
1084 let header = GeoFileHeader {
1085 flags: 0x12345678,
1086 section_table_offset: 0x1000,
1087 section_count: 5,
1088 next_data_offset: 0x2000,
1089 created_at_epoch: 1234567890,
1090 modified_at_epoch: 1234567900,
1091 ..Default::default()
1092 };
1093
1094 let encoded = encode_header(&header);
1095 let decoded = decode_header(&encoded).unwrap();
1096
1097 assert_eq!(decoded, header);
1098 }
1099
1100 #[test]
1101 fn test_decode_header_reserved_slice_correct() {
1102 let mut buf = [0u8; 128];
1103 buf[0..8].copy_from_slice(&FILE_MAGIC);
1104 buf[8..12].copy_from_slice(&1u32.to_le_bytes());
1105 buf[100] = 0x42;
1107 buf[127] = 0xFF;
1108
1109 let decoded = decode_header(&buf).unwrap();
1110
1111 assert_eq!(decoded.reserved[100 - 56], 0x42);
1113 assert_eq!(decoded.reserved[127 - 56], 0xFF);
1114 }
1115
1116 #[test]
1117 fn test_invalid_magic_rejected() {
1118 let mut buf = [0u8; 128];
1119 buf[0..8].copy_from_slice(b"BADMAGIC");
1120 assert!(decode_header(&buf).is_err());
1121 }
1122
1123 #[test]
1126 fn test_section_entry_exactly_64_bytes() {
1127 let entry = SectionEntry {
1128 name: "test".to_string(),
1129 offset: 0,
1130 length: 0,
1131 capacity: 0,
1132 flags: 0,
1133 checksum: 0,
1134 };
1135 let encoded = encode_section_entry(&entry);
1136 assert_eq!(encoded.len(), 64, "Section entry must be exactly 64 bytes");
1137 }
1138
1139 #[test]
1140 fn test_section_entry_roundtrip() {
1141 let entry = SectionEntry {
1142 name: "test_section".to_string(),
1143 offset: 1024,
1144 length: 512,
1145 capacity: 1024,
1146 flags: 0x12345678,
1147 checksum: 0xABCDEF01,
1148 };
1149 let encoded = encode_section_entry(&entry);
1150 assert_eq!(encoded.len(), 64);
1151 let decoded = decode_section_entry(&encoded).unwrap();
1152 assert_eq!(entry.name, decoded.name);
1153 assert_eq!(entry.offset, decoded.offset);
1154 assert_eq!(entry.length, decoded.length);
1155 assert_eq!(entry.capacity, decoded.capacity);
1156 assert_eq!(entry.flags, decoded.flags);
1157 assert_eq!(entry.checksum, decoded.checksum);
1158 }
1159
1160 #[test]
1161 fn test_section_name_encoding() {
1162 let name = "cfg_data";
1163 let encoded = encode_section_entry_name(name);
1164 let decoded = decode_section_entry_name(&encoded).unwrap();
1165 assert_eq!(name, decoded);
1166 }
1167
1168 #[test]
1171 fn test_checksum_deterministic() {
1172 let data = b"test data";
1173 let crc1 = compute_checksum(data);
1174 let crc2 = compute_checksum(data);
1175 assert_eq!(crc1, crc2);
1176 }
1177
1178 #[test]
1179 fn test_checksum_detects_corruption() {
1180 let data1 = b"test data";
1181 let data2 = b"test datb"; assert_ne!(compute_checksum(data1), compute_checksum(data2));
1183 }
1184
1185 #[test]
1186 fn test_checksum_empty() {
1187 let data = b"";
1188 let crc = compute_checksum(data);
1189 assert_eq!(crc, 0);
1191 }
1192
1193 #[test]
1196 fn test_section_name_32_bytes_accepted() {
1197 let name_32_bytes = "12345678901234567890123456789012"; assert_eq!(name_32_bytes.len(), 32);
1199
1200 let encoded = encode_section_entry_name(name_32_bytes);
1201 let decoded = decode_section_entry_name(&encoded).unwrap();
1202 assert_eq!(name_32_bytes, decoded);
1203 }
1204
1205 #[test]
1206 fn test_section_name_encoding_truncates_at_32() {
1207 let name_33_bytes = "123456789012345678901234567890123"; assert_eq!(name_33_bytes.len(), 33);
1209
1210 let encoded = encode_section_entry_name(name_33_bytes);
1211 let decoded = decode_section_entry_name(&encoded).unwrap();
1212 assert_eq!(decoded.len(), 32); assert_eq!(decoded, "12345678901234567890123456789012");
1214 }
1215}