1use crate::mat_mut::MatMut;
52use crate::mat_ref::MatRef;
53use memmap2::{Mmap, MmapMut, MmapOptions};
54use oxiblas_core::memory::DEFAULT_ALIGN;
55use oxiblas_core::scalar::Scalar;
56use std::fs::{File, OpenOptions};
57use std::io;
58use std::marker::PhantomData;
59use std::path::Path;
60
61const MAGIC: &[u8; 8] = b"OXIBLAS\0";
63
64const VERSION: u64 = 1;
66
67const HEADER_SIZE: usize = 64;
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[repr(u64)]
73pub enum ElementType {
74 F32 = 1,
76 F64 = 2,
78 C32 = 3,
80 C64 = 4,
82 I32 = 5,
84 I64 = 6,
86}
87
88impl ElementType {
89 #[inline]
91 pub const fn size(self) -> usize {
92 match self {
93 Self::F32 => 4,
94 Self::F64 => 8,
95 Self::C32 => 8,
96 Self::C64 => 16,
97 Self::I32 => 4,
98 Self::I64 => 8,
99 }
100 }
101
102 fn from_type<T: Scalar>() -> Option<Self> {
104 let size = core::mem::size_of::<T>();
105 let name = core::any::type_name::<T>();
106
107 if name.contains("f32") && size == 4 {
108 Some(Self::F32)
109 } else if name.contains("f64") && size == 8 {
110 Some(Self::F64)
111 } else if name.contains("Complex") && size == 8 {
112 Some(Self::C32)
113 } else if name.contains("Complex") && size == 16 {
114 Some(Self::C64)
115 } else if name.contains("i32") && size == 4 {
116 Some(Self::I32)
117 } else if name.contains("i64") && size == 8 {
118 Some(Self::I64)
119 } else {
120 None
121 }
122 }
123
124 fn from_u64(v: u64) -> Option<Self> {
125 match v {
126 1 => Some(Self::F32),
127 2 => Some(Self::F64),
128 3 => Some(Self::C32),
129 4 => Some(Self::C64),
130 5 => Some(Self::I32),
131 6 => Some(Self::I64),
132 _ => None,
133 }
134 }
135}
136
137#[derive(Debug)]
139pub enum MmapError {
140 Io(io::Error),
142 InvalidFormat(String),
144 TypeMismatch {
146 expected: ElementType,
148 found: ElementType,
150 },
151 UnsupportedType,
153 InvalidDimensions(String),
155}
156
157impl std::fmt::Display for MmapError {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 match self {
160 Self::Io(e) => write!(f, "I/O error: {e}"),
161 Self::InvalidFormat(msg) => write!(f, "Invalid format: {msg}"),
162 Self::TypeMismatch { expected, found } => {
163 write!(f, "Type mismatch: expected {expected:?}, found {found:?}")
164 }
165 Self::UnsupportedType => write!(f, "Unsupported element type"),
166 Self::InvalidDimensions(msg) => write!(f, "Invalid dimensions: {msg}"),
167 }
168 }
169}
170
171impl std::error::Error for MmapError {
172 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
173 match self {
174 Self::Io(e) => Some(e),
175 _ => None,
176 }
177 }
178}
179
180impl From<io::Error> for MmapError {
181 fn from(e: io::Error) -> Self {
182 Self::Io(e)
183 }
184}
185
186#[repr(C)]
188struct Header {
189 magic: [u8; 8],
190 version: u64,
191 elem_type: u64,
192 nrows: u64,
193 ncols: u64,
194 row_stride: u64,
195 _padding: [u8; 16], }
197
198impl Header {
199 fn new<T: Scalar>(nrows: usize, ncols: usize, row_stride: usize) -> Result<Self, MmapError> {
200 let elem_type = ElementType::from_type::<T>().ok_or(MmapError::UnsupportedType)?;
201
202 Ok(Header {
203 magic: *MAGIC,
204 version: VERSION,
205 elem_type: elem_type as u64,
206 nrows: nrows as u64,
207 ncols: ncols as u64,
208 row_stride: row_stride as u64,
209 _padding: [0; 16],
210 })
211 }
212
213 fn validate<T: Scalar>(&self) -> Result<(), MmapError> {
214 if &self.magic != MAGIC {
216 return Err(MmapError::InvalidFormat("Invalid magic number".to_string()));
217 }
218
219 if self.version != VERSION {
221 return Err(MmapError::InvalidFormat(format!(
222 "Unsupported version: {}",
223 self.version
224 )));
225 }
226
227 let file_type = ElementType::from_u64(self.elem_type).ok_or(MmapError::InvalidFormat(
229 format!("Unknown element type: {}", self.elem_type),
230 ))?;
231
232 let expected_type = ElementType::from_type::<T>().ok_or(MmapError::UnsupportedType)?;
233
234 if file_type != expected_type {
235 return Err(MmapError::TypeMismatch {
236 expected: expected_type,
237 found: file_type,
238 });
239 }
240
241 Ok(())
242 }
243
244 fn validate_layout<T: Scalar>(&self, mmap_len: usize) -> Result<(), MmapError> {
264 let nrows = self.nrows as usize;
265 let ncols = self.ncols as usize;
266 let row_stride = self.row_stride as usize;
267
268 if row_stride < nrows {
271 return Err(MmapError::InvalidDimensions(format!(
272 "row_stride ({row_stride}) is smaller than nrows ({nrows})"
273 )));
274 }
275
276 let required = row_stride
278 .checked_mul(ncols)
279 .and_then(|elems| elems.checked_mul(core::mem::size_of::<T>()))
280 .and_then(|data_bytes| data_bytes.checked_add(HEADER_SIZE))
281 .ok_or_else(|| {
282 MmapError::InvalidDimensions(format!(
283 "dimensions overflow usize: nrows={nrows}, ncols={ncols}, row_stride={row_stride}"
284 ))
285 })?;
286
287 if mmap_len < required {
288 return Err(MmapError::InvalidDimensions(format!(
289 "file too small: {mmap_len} bytes present, {required} required for \
290 {nrows}x{ncols} matrix (row_stride={row_stride})"
291 )));
292 }
293
294 Ok(())
295 }
296
297 fn to_bytes(&self) -> [u8; HEADER_SIZE] {
298 let mut bytes = [0u8; HEADER_SIZE];
299 bytes[0..8].copy_from_slice(&self.magic);
300 bytes[8..16].copy_from_slice(&self.version.to_le_bytes());
301 bytes[16..24].copy_from_slice(&self.elem_type.to_le_bytes());
302 bytes[24..32].copy_from_slice(&self.nrows.to_le_bytes());
303 bytes[32..40].copy_from_slice(&self.ncols.to_le_bytes());
304 bytes[40..48].copy_from_slice(&self.row_stride.to_le_bytes());
305 bytes
306 }
307
308 fn from_bytes(bytes: &[u8]) -> Result<Self, MmapError> {
309 if bytes.len() < HEADER_SIZE {
310 return Err(MmapError::InvalidFormat("Header too short".to_string()));
311 }
312
313 let mut magic = [0u8; 8];
314 magic.copy_from_slice(&bytes[0..8]);
315
316 Ok(Header {
317 magic,
318 version: u64::from_le_bytes(bytes[8..16].try_into().expect("slice is exactly 8 bytes")),
319 elem_type: u64::from_le_bytes(
320 bytes[16..24].try_into().expect("slice is exactly 8 bytes"),
321 ),
322 nrows: u64::from_le_bytes(bytes[24..32].try_into().expect("slice is exactly 8 bytes")),
323 ncols: u64::from_le_bytes(bytes[32..40].try_into().expect("slice is exactly 8 bytes")),
324 row_stride: u64::from_le_bytes(
325 bytes[40..48].try_into().expect("slice is exactly 8 bytes"),
326 ),
327 _padding: [0; 16],
328 })
329 }
330}
331
332fn compute_row_stride<T>(nrows: usize) -> Result<usize, MmapError> {
347 if nrows == 0 {
348 return Ok(0);
349 }
350
351 let elem_size = core::mem::size_of::<T>();
352 let elems_per_cacheline = DEFAULT_ALIGN / elem_size;
353
354 nrows
355 .div_ceil(elems_per_cacheline)
356 .checked_mul(elems_per_cacheline)
357 .ok_or_else(|| {
358 MmapError::InvalidDimensions(format!(
359 "row stride overflow: nrows={nrows} cannot be padded to a multiple \
360 of {elems_per_cacheline} elements without exceeding usize::MAX"
361 ))
362 })
363}
364
365pub struct MmapMat<T: Scalar> {
370 mmap: Mmap,
371 nrows: usize,
372 ncols: usize,
373 row_stride: usize,
374 _phantom: PhantomData<T>,
375}
376
377impl<T: Scalar> MmapMat<T> {
378 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MmapError> {
387 let file = File::open(path)?;
388 let mmap = unsafe { MmapOptions::new().map(&file)? };
389
390 let header = Header::from_bytes(&mmap)?;
392 header.validate::<T>()?;
393 header.validate_layout::<T>(mmap.len())?;
396
397 Ok(MmapMat {
398 mmap,
399 nrows: header.nrows as usize,
400 ncols: header.ncols as usize,
401 row_stride: header.row_stride as usize,
402 _phantom: PhantomData,
403 })
404 }
405
406 #[inline]
408 pub fn nrows(&self) -> usize {
409 self.nrows
410 }
411
412 #[inline]
414 pub fn ncols(&self) -> usize {
415 self.ncols
416 }
417
418 #[inline]
420 pub fn shape(&self) -> (usize, usize) {
421 (self.nrows, self.ncols)
422 }
423
424 #[inline]
426 pub fn row_stride(&self) -> usize {
427 self.row_stride
428 }
429
430 #[inline]
432 pub fn as_ptr(&self) -> *const T {
433 unsafe { self.mmap.as_ptr().add(HEADER_SIZE).cast() }
434 }
435
436 #[inline]
438 pub fn as_ref(&self) -> MatRef<'_, T> {
439 unsafe { MatRef::new(self.as_ptr(), self.nrows, self.ncols, self.row_stride) }
443 }
444
445 #[inline]
447 pub fn get(&self, row: usize, col: usize) -> Option<&T> {
448 if row < self.nrows && col < self.ncols {
449 Some(unsafe { &*self.as_ptr().add(row + col * self.row_stride) })
450 } else {
451 None
452 }
453 }
454
455 #[cfg(unix)]
459 pub fn advise_sequential(&self) -> Result<(), MmapError> {
460 self.mmap.advise(memmap2::Advice::Sequential)?;
461 Ok(())
462 }
463
464 #[cfg(unix)]
466 pub fn advise_willneed(&self) -> Result<(), MmapError> {
467 self.mmap.advise(memmap2::Advice::WillNeed)?;
468 Ok(())
469 }
470}
471
472impl<T: Scalar> core::ops::Index<(usize, usize)> for MmapMat<T> {
473 type Output = T;
474
475 #[inline]
476 fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
477 assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
478 unsafe { &*self.as_ptr().add(row + col * self.row_stride) }
479 }
480}
481
482pub struct MmapMatMut<T: Scalar> {
487 mmap: MmapMut,
488 nrows: usize,
489 ncols: usize,
490 row_stride: usize,
491 _phantom: PhantomData<T>,
492}
493
494impl<T: Scalar> MmapMatMut<T> {
495 pub fn create<P: AsRef<Path>>(path: P, nrows: usize, ncols: usize) -> Result<Self, MmapError> {
505 let row_stride = compute_row_stride::<T>(nrows)?;
506 let total_size = row_stride
514 .checked_mul(ncols)
515 .and_then(|elems| elems.checked_mul(core::mem::size_of::<T>()))
516 .and_then(|data_bytes| data_bytes.checked_add(HEADER_SIZE))
517 .ok_or_else(|| {
518 MmapError::InvalidDimensions(format!(
519 "dimensions overflow usize: nrows={nrows}, ncols={ncols}, \
520 row_stride={row_stride}"
521 ))
522 })?;
523
524 let file = OpenOptions::new()
526 .read(true)
527 .write(true)
528 .create(true)
529 .truncate(true)
530 .open(path)?;
531
532 file.set_len(total_size as u64)?;
533
534 let mut mmap = unsafe { MmapOptions::new().map_mut(&file)? };
536
537 let header = Header::new::<T>(nrows, ncols, row_stride)?;
539
540 header.validate_layout::<T>(mmap.len())?;
547
548 mmap[0..HEADER_SIZE].copy_from_slice(&header.to_bytes());
549
550 mmap[HEADER_SIZE..].fill(0);
552
553 Ok(MmapMatMut {
554 mmap,
555 nrows,
556 ncols,
557 row_stride,
558 _phantom: PhantomData,
559 })
560 }
561
562 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MmapError> {
571 let file = OpenOptions::new().read(true).write(true).open(path)?;
572
573 let mmap = unsafe { MmapOptions::new().map_mut(&file)? };
574
575 let header = Header::from_bytes(&mmap)?;
577 header.validate::<T>()?;
578 header.validate_layout::<T>(mmap.len())?;
582
583 Ok(MmapMatMut {
584 mmap,
585 nrows: header.nrows as usize,
586 ncols: header.ncols as usize,
587 row_stride: header.row_stride as usize,
588 _phantom: PhantomData,
589 })
590 }
591
592 #[inline]
594 pub fn nrows(&self) -> usize {
595 self.nrows
596 }
597
598 #[inline]
600 pub fn ncols(&self) -> usize {
601 self.ncols
602 }
603
604 #[inline]
606 pub fn shape(&self) -> (usize, usize) {
607 (self.nrows, self.ncols)
608 }
609
610 #[inline]
612 pub fn row_stride(&self) -> usize {
613 self.row_stride
614 }
615
616 #[inline]
618 pub fn as_ptr(&self) -> *const T {
619 unsafe { self.mmap.as_ptr().add(HEADER_SIZE).cast() }
620 }
621
622 #[inline]
624 pub fn as_mut_ptr(&mut self) -> *mut T {
625 unsafe { self.mmap.as_mut_ptr().add(HEADER_SIZE).cast() }
626 }
627
628 #[inline]
630 pub fn as_ref(&self) -> MatRef<'_, T> {
631 unsafe { MatRef::new(self.as_ptr(), self.nrows, self.ncols, self.row_stride) }
635 }
636
637 #[inline]
639 pub fn as_mut(&mut self) -> MatMut<'_, T> {
640 unsafe { MatMut::new(self.as_mut_ptr(), self.nrows, self.ncols, self.row_stride) }
646 }
647
648 #[inline]
650 pub fn get(&self, row: usize, col: usize) -> Option<&T> {
651 if row < self.nrows && col < self.ncols {
652 Some(unsafe { &*self.as_ptr().add(row + col * self.row_stride) })
653 } else {
654 None
655 }
656 }
657
658 #[inline]
660 pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> {
661 if row < self.nrows && col < self.ncols {
662 Some(unsafe { &mut *self.as_mut_ptr().add(row + col * self.row_stride) })
663 } else {
664 None
665 }
666 }
667
668 #[inline]
670 pub fn set(&mut self, row: usize, col: usize, value: T) {
671 assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
672 unsafe {
673 *self.as_mut_ptr().add(row + col * self.row_stride) = value;
674 }
675 }
676
677 pub fn flush(&self) -> Result<(), MmapError> {
681 self.mmap.flush()?;
682 Ok(())
683 }
684
685 pub fn flush_async(&self) -> Result<(), MmapError> {
689 self.mmap.flush_async()?;
690 Ok(())
691 }
692
693 pub fn fill(&mut self, value: T) {
695 for j in 0..self.ncols {
696 for i in 0..self.nrows {
697 self.set(i, j, value);
698 }
699 }
700 }
701
702 pub fn copy_from(&mut self, src: &MatRef<'_, T>) {
704 assert_eq!(
705 self.shape(),
706 src.shape(),
707 "Matrix shapes must match for copy"
708 );
709
710 for j in 0..self.ncols {
711 for i in 0..self.nrows {
712 self.set(i, j, src[(i, j)]);
713 }
714 }
715 }
716
717 #[cfg(unix)]
719 pub fn advise_sequential(&self) -> Result<(), MmapError> {
720 self.mmap.advise(memmap2::Advice::Sequential)?;
721 Ok(())
722 }
723
724 #[cfg(unix)]
726 pub fn advise_willneed(&self) -> Result<(), MmapError> {
727 self.mmap.advise(memmap2::Advice::WillNeed)?;
728 Ok(())
729 }
730}
731
732impl<T: Scalar> core::ops::Index<(usize, usize)> for MmapMatMut<T> {
733 type Output = T;
734
735 #[inline]
736 fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
737 assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
738 unsafe { &*self.as_ptr().add(row + col * self.row_stride) }
739 }
740}
741
742impl<T: Scalar> core::ops::IndexMut<(usize, usize)> for MmapMatMut<T> {
743 #[inline]
744 fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output {
745 assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
746 unsafe { &mut *self.as_mut_ptr().add(row + col * self.row_stride) }
747 }
748}
749
750pub struct MmapBuilder<T: Scalar> {
752 nrows: usize,
753 ncols: usize,
754 _phantom: PhantomData<T>,
755}
756
757impl<T: Scalar> MmapBuilder<T> {
758 pub fn new(nrows: usize, ncols: usize) -> Self {
760 MmapBuilder {
761 nrows,
762 ncols,
763 _phantom: PhantomData,
764 }
765 }
766
767 pub fn from_mat<P: AsRef<Path>>(
769 self,
770 path: P,
771 mat: &MatRef<'_, T>,
772 ) -> Result<MmapMatMut<T>, MmapError> {
773 if mat.shape() != (self.nrows, self.ncols) {
774 return Err(MmapError::InvalidDimensions(format!(
775 "Builder dimensions ({}, {}) don't match matrix ({}, {})",
776 self.nrows,
777 self.ncols,
778 mat.nrows(),
779 mat.ncols()
780 )));
781 }
782
783 let mut mmat = MmapMatMut::create(path, self.nrows, self.ncols)?;
784 mmat.copy_from(mat);
785 mmat.flush()?;
786 Ok(mmat)
787 }
788
789 pub fn from_slice<P: AsRef<Path>>(
791 self,
792 path: P,
793 data: &[T],
794 ) -> Result<MmapMatMut<T>, MmapError> {
795 let expected_len = self.nrows * self.ncols;
796 if data.len() != expected_len {
797 return Err(MmapError::InvalidDimensions(format!(
798 "Slice length {} doesn't match dimensions {} x {} = {}",
799 data.len(),
800 self.nrows,
801 self.ncols,
802 expected_len
803 )));
804 }
805
806 let mut mmat = MmapMatMut::create(path, self.nrows, self.ncols)?;
807
808 for j in 0..self.ncols {
810 for i in 0..self.nrows {
811 mmat.set(i, j, data[i + j * self.nrows]);
812 }
813 }
814
815 mmat.flush()?;
816 Ok(mmat)
817 }
818}
819
820pub fn write_mat<T: Scalar, P: AsRef<Path>>(path: P, mat: &MatRef<'_, T>) -> Result<(), MmapError> {
822 let mut mmat = MmapMatMut::create(path, mat.nrows(), mat.ncols())?;
823 mmat.copy_from(mat);
824 mmat.flush()?;
825 Ok(())
826}
827
828pub fn read_dimensions<P: AsRef<Path>>(path: P) -> Result<(usize, usize), MmapError> {
830 let mut file = File::open(path)?;
831 let mut header_bytes = [0u8; HEADER_SIZE];
832
833 use std::io::Read;
834 file.read_exact(&mut header_bytes)?;
835
836 let header = Header::from_bytes(&header_bytes)?;
837
838 if &header.magic != MAGIC {
840 return Err(MmapError::InvalidFormat("Invalid magic number".to_string()));
841 }
842 if header.version != VERSION {
843 return Err(MmapError::InvalidFormat(format!(
844 "Unsupported version: {}",
845 header.version
846 )));
847 }
848
849 Ok((header.nrows as usize, header.ncols as usize))
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
857 fn test_mmap_create_and_open() {
858 let dir = std::env::temp_dir();
859 let path = dir.join("test_mmap_basic.oxiblas");
860
861 {
863 let mut mmat = MmapMatMut::<f64>::create(&path, 10, 10).unwrap();
864 for i in 0..10 {
865 for j in 0..10 {
866 mmat[(i, j)] = (i * 10 + j) as f64;
867 }
868 }
869 mmat.flush().unwrap();
870 }
871
872 {
874 let mmat = MmapMat::<f64>::open(&path).unwrap();
875 assert_eq!(mmat.shape(), (10, 10));
876 for i in 0..10 {
877 for j in 0..10 {
878 assert_eq!(mmat[(i, j)], (i * 10 + j) as f64);
879 }
880 }
881 }
882
883 std::fs::remove_file(path).ok();
884 }
885
886 #[test]
887 fn test_mmap_views() {
888 let dir = std::env::temp_dir();
889 let path = dir.join("test_mmap_views.oxiblas");
890
891 let mut mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
892
893 {
895 let mut view = mmat.as_mut();
896 for i in 0..5 {
897 view[(i, i)] = 1.0;
898 }
899 }
900
901 {
903 let view = mmat.as_ref();
904 for i in 0..5 {
905 for j in 0..5 {
906 if i == j {
907 assert_eq!(view[(i, j)], 1.0);
908 } else {
909 assert_eq!(view[(i, j)], 0.0);
910 }
911 }
912 }
913 }
914
915 std::fs::remove_file(path).ok();
916 }
917
918 #[test]
919 fn test_mmap_f32() {
920 let dir = std::env::temp_dir();
921 let path = dir.join("test_mmap_f32.oxiblas");
922
923 {
924 let mut mmat = MmapMatMut::<f32>::create(&path, 3, 3).unwrap();
925 mmat[(0, 0)] = 1.0f32;
926 mmat[(1, 1)] = 2.0f32;
927 mmat[(2, 2)] = 3.0f32;
928 mmat.flush().unwrap();
929 }
930
931 {
932 let mmat = MmapMat::<f32>::open(&path).unwrap();
933 assert_eq!(mmat[(0, 0)], 1.0f32);
934 assert_eq!(mmat[(1, 1)], 2.0f32);
935 assert_eq!(mmat[(2, 2)], 3.0f32);
936 }
937
938 std::fs::remove_file(path).ok();
939 }
940
941 #[test]
942 fn test_mmap_type_mismatch() {
943 let dir = std::env::temp_dir();
944 let path = dir.join("test_mmap_type_mismatch.oxiblas");
945
946 {
948 let _mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
949 }
950
951 {
953 let result = MmapMat::<f32>::open(&path);
954 assert!(result.is_err());
955 if let Err(MmapError::TypeMismatch { expected, found }) = result {
956 assert_eq!(expected, ElementType::F32);
957 assert_eq!(found, ElementType::F64);
958 } else {
959 panic!("Expected TypeMismatch error");
960 }
961 }
962
963 std::fs::remove_file(path).ok();
964 }
965
966 #[test]
967 fn test_mmap_builder() {
968 use crate::Mat;
969
970 let dir = std::env::temp_dir();
971 let path = dir.join("test_mmap_builder.oxiblas");
972
973 let mat = Mat::<f64>::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
975
976 {
978 let builder = MmapBuilder::<f64>::new(2, 3);
979 let mmat = builder.from_mat(&path, &mat.as_ref()).unwrap();
980 assert_eq!(mmat.shape(), (2, 3));
981 }
982
983 {
985 let mmat = MmapMat::<f64>::open(&path).unwrap();
986 assert_eq!(mmat[(0, 0)], 1.0);
987 assert_eq!(mmat[(0, 2)], 3.0);
988 assert_eq!(mmat[(1, 0)], 4.0);
989 assert_eq!(mmat[(1, 2)], 6.0);
990 }
991
992 std::fs::remove_file(path).ok();
993 }
994
995 #[test]
996 fn test_read_dimensions() {
997 let dir = std::env::temp_dir();
998 let path = dir.join("test_read_dims.oxiblas");
999
1000 {
1001 let _mmat = MmapMatMut::<f64>::create(&path, 100, 200).unwrap();
1002 }
1003
1004 let (nrows, ncols) = read_dimensions(&path).unwrap();
1005 assert_eq!(nrows, 100);
1006 assert_eq!(ncols, 200);
1007
1008 std::fs::remove_file(path).ok();
1009 }
1010
1011 #[test]
1012 fn test_mmap_large_matrix() {
1013 let dir = std::env::temp_dir();
1014 let path = dir.join("test_mmap_large.oxiblas");
1015
1016 let nrows = 1000;
1017 let ncols = 500;
1018
1019 {
1021 let mut mmat = MmapMatMut::<f64>::create(&path, nrows, ncols).unwrap();
1022
1023 for i in 0..nrows.min(ncols) {
1025 mmat[(i, i)] = (i + 1) as f64;
1026 }
1027
1028 mmat[(0, 0)] = -1.0;
1030 mmat[(nrows - 1, ncols - 1)] = -2.0;
1031
1032 mmat.flush().unwrap();
1033 }
1034
1035 {
1037 let mmat = MmapMat::<f64>::open(&path).unwrap();
1038 assert_eq!(mmat.shape(), (nrows, ncols));
1039 assert_eq!(mmat[(0, 0)], -1.0);
1040 assert_eq!(mmat[(nrows - 1, ncols - 1)], -2.0);
1041 assert_eq!(mmat[(100, 100)], 101.0);
1042 }
1043
1044 std::fs::remove_file(path).ok();
1045 }
1046
1047 #[test]
1048 fn test_mmap_fill() {
1049 let dir = std::env::temp_dir();
1050 let path = dir.join("test_mmap_fill.oxiblas");
1051
1052 {
1053 let mut mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
1054 mmat.fill(42.0);
1055 mmat.flush().unwrap();
1056 }
1057
1058 {
1059 let mmat = MmapMat::<f64>::open(&path).unwrap();
1060 for i in 0..5 {
1061 for j in 0..5 {
1062 assert_eq!(mmat[(i, j)], 42.0);
1063 }
1064 }
1065 }
1066
1067 std::fs::remove_file(path).ok();
1068 }
1069
1070 #[test]
1071 fn test_write_mat() {
1072 use crate::Mat;
1073
1074 let dir = std::env::temp_dir();
1075 let path = dir.join("test_write_mat.oxiblas");
1076
1077 let mat = Mat::<f64>::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1078
1079 write_mat(&path, &mat.as_ref()).unwrap();
1080
1081 let mmat = MmapMat::<f64>::open(&path).unwrap();
1082 assert_eq!(mmat[(0, 0)], 1.0);
1083 assert_eq!(mmat[(0, 1)], 2.0);
1084 assert_eq!(mmat[(1, 0)], 3.0);
1085 assert_eq!(mmat[(1, 1)], 4.0);
1086
1087 std::fs::remove_file(path).ok();
1088 }
1089
1090 fn write_oxiblas_with_short_data(
1096 path: &std::path::Path,
1097 nrows: usize,
1098 ncols: usize,
1099 row_stride: usize,
1100 data_bytes: usize,
1101 ) {
1102 use std::io::Write;
1103 let header =
1104 Header::new::<f64>(nrows, ncols, row_stride).expect("f64 is a supported element type");
1105 let mut file = std::fs::File::create(path).expect("create temp file");
1106 file.write_all(&header.to_bytes()).expect("write header");
1107 file.write_all(&vec![0u8; data_bytes])
1108 .expect("write short data section");
1109 file.flush().expect("flush temp file");
1110 }
1111
1112 #[test]
1113 fn test_mmap_open_truncated_rejected() {
1114 let dir = std::env::temp_dir();
1115 let path = dir.join("test_mmap_open_truncated_ro.oxiblas");
1116
1117 write_oxiblas_with_short_data(&path, 100, 100, 104, 64);
1122
1123 match MmapMat::<f64>::open(&path) {
1124 Err(MmapError::InvalidDimensions(_)) => {}
1125 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1126 Ok(_) => panic!("truncated read-only file was accepted (out-of-bounds read hazard)"),
1127 }
1128
1129 std::fs::remove_file(path).ok();
1130 }
1131
1132 #[test]
1133 fn test_mmap_mut_open_truncated_rejected() {
1134 let dir = std::env::temp_dir();
1135 let path = dir.join("test_mmap_open_truncated_rw.oxiblas");
1136
1137 write_oxiblas_with_short_data(&path, 100, 100, 104, 64);
1141
1142 match MmapMatMut::<f64>::open(&path) {
1143 Err(MmapError::InvalidDimensions(_)) => {}
1144 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1145 Ok(_) => panic!("truncated writable file was accepted (out-of-bounds write hazard)"),
1146 }
1147
1148 std::fs::remove_file(path).ok();
1149 }
1150
1151 #[test]
1152 fn test_mmap_open_bad_row_stride_rejected() {
1153 let dir = std::env::temp_dir();
1154 let path = dir.join("test_mmap_open_bad_stride.oxiblas");
1155
1156 write_oxiblas_with_short_data(&path, 100, 10, 4, 4 * 10 * 8);
1159
1160 match MmapMat::<f64>::open(&path) {
1161 Err(MmapError::InvalidDimensions(_)) => {}
1162 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1163 Ok(_) => panic!("file with row_stride < nrows was accepted"),
1164 }
1165
1166 std::fs::remove_file(path).ok();
1167 }
1168
1169 #[test]
1170 fn test_mmap_open_overflow_dims_rejected() {
1171 let dir = std::env::temp_dir();
1172 let path = dir.join("test_mmap_open_overflow.oxiblas");
1173
1174 write_oxiblas_with_short_data(&path, usize::MAX, 1024, usize::MAX, 64);
1178
1179 match MmapMat::<f64>::open(&path) {
1180 Err(MmapError::InvalidDimensions(_)) => {}
1181 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1182 Ok(_) => panic!("file with overflowing dimensions was accepted"),
1183 }
1184
1185 std::fs::remove_file(path).ok();
1186 }
1187
1188 #[test]
1198 fn test_mmap_create_overflow_dims_rejected() {
1199 let dir = std::env::temp_dir();
1200 let path = dir.join("test_mmap_create_overflow.oxiblas");
1201
1202 match MmapMatMut::<f64>::create(&path, 1usize << 32, 1usize << 32) {
1204 Err(MmapError::InvalidDimensions(_)) => {}
1205 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1206 Ok(_) => panic!("create() accepted overflowing dimensions"),
1207 }
1208
1209 std::fs::remove_file(path).ok();
1210 }
1211
1212 #[test]
1213 fn test_mmap_create_row_stride_overflow_rejected() {
1214 let dir = std::env::temp_dir();
1215 let path = dir.join("test_mmap_create_stride_overflow.oxiblas");
1216
1217 match MmapMatMut::<f64>::create(&path, usize::MAX, 1) {
1220 Err(MmapError::InvalidDimensions(_)) => {}
1221 Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
1222 Ok(_) => panic!("create() accepted an overflowing row stride"),
1223 }
1224
1225 std::fs::remove_file(path).ok();
1226 }
1227
1228 #[test]
1229 fn test_mmap_create_sane_dims_still_work() {
1230 let dir = std::env::temp_dir();
1232 let path = dir.join("test_mmap_create_sane.oxiblas");
1233
1234 {
1235 let mut m = MmapMatMut::<f64>::create(&path, 8, 4)
1236 .expect("creating an 8x4 matrix must succeed");
1237 m.set(0, 0, 1.5);
1238 m.set(7, 3, 2.5);
1239 assert_eq!(m.get(0, 0), Some(&1.5));
1240 assert_eq!(m.get(7, 3), Some(&2.5));
1241 }
1242
1243 let reopened = MmapMat::<f64>::open(&path).expect("reopening must succeed");
1244 assert_eq!(reopened.shape(), (8, 4));
1245 assert_eq!(reopened.get(7, 3), Some(&2.5));
1246
1247 std::fs::remove_file(path).ok();
1248 }
1249}