use crate::mat_mut::MatMut;
use crate::mat_ref::MatRef;
use memmap2::{Mmap, MmapMut, MmapOptions};
use oxiblas_core::memory::DEFAULT_ALIGN;
use oxiblas_core::scalar::Scalar;
use std::fs::{File, OpenOptions};
use std::io;
use std::marker::PhantomData;
use std::path::Path;
const MAGIC: &[u8; 8] = b"OXIBLAS\0";
const VERSION: u64 = 1;
const HEADER_SIZE: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u64)]
pub enum ElementType {
F32 = 1,
F64 = 2,
C32 = 3,
C64 = 4,
I32 = 5,
I64 = 6,
}
impl ElementType {
#[inline]
pub const fn size(self) -> usize {
match self {
Self::F32 => 4,
Self::F64 => 8,
Self::C32 => 8,
Self::C64 => 16,
Self::I32 => 4,
Self::I64 => 8,
}
}
fn from_type<T: Scalar>() -> Option<Self> {
let size = core::mem::size_of::<T>();
let name = core::any::type_name::<T>();
if name.contains("f32") && size == 4 {
Some(Self::F32)
} else if name.contains("f64") && size == 8 {
Some(Self::F64)
} else if name.contains("Complex") && size == 8 {
Some(Self::C32)
} else if name.contains("Complex") && size == 16 {
Some(Self::C64)
} else if name.contains("i32") && size == 4 {
Some(Self::I32)
} else if name.contains("i64") && size == 8 {
Some(Self::I64)
} else {
None
}
}
fn from_u64(v: u64) -> Option<Self> {
match v {
1 => Some(Self::F32),
2 => Some(Self::F64),
3 => Some(Self::C32),
4 => Some(Self::C64),
5 => Some(Self::I32),
6 => Some(Self::I64),
_ => None,
}
}
}
#[derive(Debug)]
pub enum MmapError {
Io(io::Error),
InvalidFormat(String),
TypeMismatch {
expected: ElementType,
found: ElementType,
},
UnsupportedType,
InvalidDimensions(String),
}
impl std::fmt::Display for MmapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "I/O error: {e}"),
Self::InvalidFormat(msg) => write!(f, "Invalid format: {msg}"),
Self::TypeMismatch { expected, found } => {
write!(f, "Type mismatch: expected {expected:?}, found {found:?}")
}
Self::UnsupportedType => write!(f, "Unsupported element type"),
Self::InvalidDimensions(msg) => write!(f, "Invalid dimensions: {msg}"),
}
}
}
impl std::error::Error for MmapError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
_ => None,
}
}
}
impl From<io::Error> for MmapError {
fn from(e: io::Error) -> Self {
Self::Io(e)
}
}
#[repr(C)]
struct Header {
magic: [u8; 8],
version: u64,
elem_type: u64,
nrows: u64,
ncols: u64,
row_stride: u64,
_padding: [u8; 16], }
impl Header {
fn new<T: Scalar>(nrows: usize, ncols: usize, row_stride: usize) -> Result<Self, MmapError> {
let elem_type = ElementType::from_type::<T>().ok_or(MmapError::UnsupportedType)?;
Ok(Header {
magic: *MAGIC,
version: VERSION,
elem_type: elem_type as u64,
nrows: nrows as u64,
ncols: ncols as u64,
row_stride: row_stride as u64,
_padding: [0; 16],
})
}
fn validate<T: Scalar>(&self) -> Result<(), MmapError> {
if &self.magic != MAGIC {
return Err(MmapError::InvalidFormat("Invalid magic number".to_string()));
}
if self.version != VERSION {
return Err(MmapError::InvalidFormat(format!(
"Unsupported version: {}",
self.version
)));
}
let file_type = ElementType::from_u64(self.elem_type).ok_or(MmapError::InvalidFormat(
format!("Unknown element type: {}", self.elem_type),
))?;
let expected_type = ElementType::from_type::<T>().ok_or(MmapError::UnsupportedType)?;
if file_type != expected_type {
return Err(MmapError::TypeMismatch {
expected: expected_type,
found: file_type,
});
}
Ok(())
}
fn validate_layout<T: Scalar>(&self, mmap_len: usize) -> Result<(), MmapError> {
let nrows = self.nrows as usize;
let ncols = self.ncols as usize;
let row_stride = self.row_stride as usize;
if row_stride < nrows {
return Err(MmapError::InvalidDimensions(format!(
"row_stride ({row_stride}) is smaller than nrows ({nrows})"
)));
}
let required = row_stride
.checked_mul(ncols)
.and_then(|elems| elems.checked_mul(core::mem::size_of::<T>()))
.and_then(|data_bytes| data_bytes.checked_add(HEADER_SIZE))
.ok_or_else(|| {
MmapError::InvalidDimensions(format!(
"dimensions overflow usize: nrows={nrows}, ncols={ncols}, row_stride={row_stride}"
))
})?;
if mmap_len < required {
return Err(MmapError::InvalidDimensions(format!(
"file too small: {mmap_len} bytes present, {required} required for \
{nrows}x{ncols} matrix (row_stride={row_stride})"
)));
}
Ok(())
}
fn to_bytes(&self) -> [u8; HEADER_SIZE] {
let mut bytes = [0u8; HEADER_SIZE];
bytes[0..8].copy_from_slice(&self.magic);
bytes[8..16].copy_from_slice(&self.version.to_le_bytes());
bytes[16..24].copy_from_slice(&self.elem_type.to_le_bytes());
bytes[24..32].copy_from_slice(&self.nrows.to_le_bytes());
bytes[32..40].copy_from_slice(&self.ncols.to_le_bytes());
bytes[40..48].copy_from_slice(&self.row_stride.to_le_bytes());
bytes
}
fn from_bytes(bytes: &[u8]) -> Result<Self, MmapError> {
if bytes.len() < HEADER_SIZE {
return Err(MmapError::InvalidFormat("Header too short".to_string()));
}
let mut magic = [0u8; 8];
magic.copy_from_slice(&bytes[0..8]);
Ok(Header {
magic,
version: u64::from_le_bytes(bytes[8..16].try_into().expect("slice is exactly 8 bytes")),
elem_type: u64::from_le_bytes(
bytes[16..24].try_into().expect("slice is exactly 8 bytes"),
),
nrows: u64::from_le_bytes(bytes[24..32].try_into().expect("slice is exactly 8 bytes")),
ncols: u64::from_le_bytes(bytes[32..40].try_into().expect("slice is exactly 8 bytes")),
row_stride: u64::from_le_bytes(
bytes[40..48].try_into().expect("slice is exactly 8 bytes"),
),
_padding: [0; 16],
})
}
}
fn compute_row_stride<T>(nrows: usize) -> Result<usize, MmapError> {
if nrows == 0 {
return Ok(0);
}
let elem_size = core::mem::size_of::<T>();
let elems_per_cacheline = DEFAULT_ALIGN / elem_size;
nrows
.div_ceil(elems_per_cacheline)
.checked_mul(elems_per_cacheline)
.ok_or_else(|| {
MmapError::InvalidDimensions(format!(
"row stride overflow: nrows={nrows} cannot be padded to a multiple \
of {elems_per_cacheline} elements without exceeding usize::MAX"
))
})
}
pub struct MmapMat<T: Scalar> {
mmap: Mmap,
nrows: usize,
ncols: usize,
row_stride: usize,
_phantom: PhantomData<T>,
}
impl<T: Scalar> MmapMat<T> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MmapError> {
let file = File::open(path)?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
let header = Header::from_bytes(&mmap)?;
header.validate::<T>()?;
header.validate_layout::<T>(mmap.len())?;
Ok(MmapMat {
mmap,
nrows: header.nrows as usize,
ncols: header.ncols as usize,
row_stride: header.row_stride as usize,
_phantom: PhantomData,
})
}
#[inline]
pub fn nrows(&self) -> usize {
self.nrows
}
#[inline]
pub fn ncols(&self) -> usize {
self.ncols
}
#[inline]
pub fn shape(&self) -> (usize, usize) {
(self.nrows, self.ncols)
}
#[inline]
pub fn row_stride(&self) -> usize {
self.row_stride
}
#[inline]
pub fn as_ptr(&self) -> *const T {
unsafe { self.mmap.as_ptr().add(HEADER_SIZE).cast() }
}
#[inline]
pub fn as_ref(&self) -> MatRef<'_, T> {
unsafe { MatRef::new(self.as_ptr(), self.nrows, self.ncols, self.row_stride) }
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> Option<&T> {
if row < self.nrows && col < self.ncols {
Some(unsafe { &*self.as_ptr().add(row + col * self.row_stride) })
} else {
None
}
}
#[cfg(unix)]
pub fn advise_sequential(&self) -> Result<(), MmapError> {
self.mmap.advise(memmap2::Advice::Sequential)?;
Ok(())
}
#[cfg(unix)]
pub fn advise_willneed(&self) -> Result<(), MmapError> {
self.mmap.advise(memmap2::Advice::WillNeed)?;
Ok(())
}
}
impl<T: Scalar> core::ops::Index<(usize, usize)> for MmapMat<T> {
type Output = T;
#[inline]
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
unsafe { &*self.as_ptr().add(row + col * self.row_stride) }
}
}
pub struct MmapMatMut<T: Scalar> {
mmap: MmapMut,
nrows: usize,
ncols: usize,
row_stride: usize,
_phantom: PhantomData<T>,
}
impl<T: Scalar> MmapMatMut<T> {
pub fn create<P: AsRef<Path>>(path: P, nrows: usize, ncols: usize) -> Result<Self, MmapError> {
let row_stride = compute_row_stride::<T>(nrows)?;
let total_size = row_stride
.checked_mul(ncols)
.and_then(|elems| elems.checked_mul(core::mem::size_of::<T>()))
.and_then(|data_bytes| data_bytes.checked_add(HEADER_SIZE))
.ok_or_else(|| {
MmapError::InvalidDimensions(format!(
"dimensions overflow usize: nrows={nrows}, ncols={ncols}, \
row_stride={row_stride}"
))
})?;
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
file.set_len(total_size as u64)?;
let mut mmap = unsafe { MmapOptions::new().map_mut(&file)? };
let header = Header::new::<T>(nrows, ncols, row_stride)?;
header.validate_layout::<T>(mmap.len())?;
mmap[0..HEADER_SIZE].copy_from_slice(&header.to_bytes());
mmap[HEADER_SIZE..].fill(0);
Ok(MmapMatMut {
mmap,
nrows,
ncols,
row_stride,
_phantom: PhantomData,
})
}
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, MmapError> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
let mmap = unsafe { MmapOptions::new().map_mut(&file)? };
let header = Header::from_bytes(&mmap)?;
header.validate::<T>()?;
header.validate_layout::<T>(mmap.len())?;
Ok(MmapMatMut {
mmap,
nrows: header.nrows as usize,
ncols: header.ncols as usize,
row_stride: header.row_stride as usize,
_phantom: PhantomData,
})
}
#[inline]
pub fn nrows(&self) -> usize {
self.nrows
}
#[inline]
pub fn ncols(&self) -> usize {
self.ncols
}
#[inline]
pub fn shape(&self) -> (usize, usize) {
(self.nrows, self.ncols)
}
#[inline]
pub fn row_stride(&self) -> usize {
self.row_stride
}
#[inline]
pub fn as_ptr(&self) -> *const T {
unsafe { self.mmap.as_ptr().add(HEADER_SIZE).cast() }
}
#[inline]
pub fn as_mut_ptr(&mut self) -> *mut T {
unsafe { self.mmap.as_mut_ptr().add(HEADER_SIZE).cast() }
}
#[inline]
pub fn as_ref(&self) -> MatRef<'_, T> {
unsafe { MatRef::new(self.as_ptr(), self.nrows, self.ncols, self.row_stride) }
}
#[inline]
pub fn as_mut(&mut self) -> MatMut<'_, T> {
unsafe { MatMut::new(self.as_mut_ptr(), self.nrows, self.ncols, self.row_stride) }
}
#[inline]
pub fn get(&self, row: usize, col: usize) -> Option<&T> {
if row < self.nrows && col < self.ncols {
Some(unsafe { &*self.as_ptr().add(row + col * self.row_stride) })
} else {
None
}
}
#[inline]
pub fn get_mut(&mut self, row: usize, col: usize) -> Option<&mut T> {
if row < self.nrows && col < self.ncols {
Some(unsafe { &mut *self.as_mut_ptr().add(row + col * self.row_stride) })
} else {
None
}
}
#[inline]
pub fn set(&mut self, row: usize, col: usize, value: T) {
assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
unsafe {
*self.as_mut_ptr().add(row + col * self.row_stride) = value;
}
}
pub fn flush(&self) -> Result<(), MmapError> {
self.mmap.flush()?;
Ok(())
}
pub fn flush_async(&self) -> Result<(), MmapError> {
self.mmap.flush_async()?;
Ok(())
}
pub fn fill(&mut self, value: T) {
for j in 0..self.ncols {
for i in 0..self.nrows {
self.set(i, j, value);
}
}
}
pub fn copy_from(&mut self, src: &MatRef<'_, T>) {
assert_eq!(
self.shape(),
src.shape(),
"Matrix shapes must match for copy"
);
for j in 0..self.ncols {
for i in 0..self.nrows {
self.set(i, j, src[(i, j)]);
}
}
}
#[cfg(unix)]
pub fn advise_sequential(&self) -> Result<(), MmapError> {
self.mmap.advise(memmap2::Advice::Sequential)?;
Ok(())
}
#[cfg(unix)]
pub fn advise_willneed(&self) -> Result<(), MmapError> {
self.mmap.advise(memmap2::Advice::WillNeed)?;
Ok(())
}
}
impl<T: Scalar> core::ops::Index<(usize, usize)> for MmapMatMut<T> {
type Output = T;
#[inline]
fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
unsafe { &*self.as_ptr().add(row + col * self.row_stride) }
}
}
impl<T: Scalar> core::ops::IndexMut<(usize, usize)> for MmapMatMut<T> {
#[inline]
fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output {
assert!(row < self.nrows && col < self.ncols, "Index out of bounds");
unsafe { &mut *self.as_mut_ptr().add(row + col * self.row_stride) }
}
}
pub struct MmapBuilder<T: Scalar> {
nrows: usize,
ncols: usize,
_phantom: PhantomData<T>,
}
impl<T: Scalar> MmapBuilder<T> {
pub fn new(nrows: usize, ncols: usize) -> Self {
MmapBuilder {
nrows,
ncols,
_phantom: PhantomData,
}
}
pub fn from_mat<P: AsRef<Path>>(
self,
path: P,
mat: &MatRef<'_, T>,
) -> Result<MmapMatMut<T>, MmapError> {
if mat.shape() != (self.nrows, self.ncols) {
return Err(MmapError::InvalidDimensions(format!(
"Builder dimensions ({}, {}) don't match matrix ({}, {})",
self.nrows,
self.ncols,
mat.nrows(),
mat.ncols()
)));
}
let mut mmat = MmapMatMut::create(path, self.nrows, self.ncols)?;
mmat.copy_from(mat);
mmat.flush()?;
Ok(mmat)
}
pub fn from_slice<P: AsRef<Path>>(
self,
path: P,
data: &[T],
) -> Result<MmapMatMut<T>, MmapError> {
let expected_len = self.nrows * self.ncols;
if data.len() != expected_len {
return Err(MmapError::InvalidDimensions(format!(
"Slice length {} doesn't match dimensions {} x {} = {}",
data.len(),
self.nrows,
self.ncols,
expected_len
)));
}
let mut mmat = MmapMatMut::create(path, self.nrows, self.ncols)?;
for j in 0..self.ncols {
for i in 0..self.nrows {
mmat.set(i, j, data[i + j * self.nrows]);
}
}
mmat.flush()?;
Ok(mmat)
}
}
pub fn write_mat<T: Scalar, P: AsRef<Path>>(path: P, mat: &MatRef<'_, T>) -> Result<(), MmapError> {
let mut mmat = MmapMatMut::create(path, mat.nrows(), mat.ncols())?;
mmat.copy_from(mat);
mmat.flush()?;
Ok(())
}
pub fn read_dimensions<P: AsRef<Path>>(path: P) -> Result<(usize, usize), MmapError> {
let mut file = File::open(path)?;
let mut header_bytes = [0u8; HEADER_SIZE];
use std::io::Read;
file.read_exact(&mut header_bytes)?;
let header = Header::from_bytes(&header_bytes)?;
if &header.magic != MAGIC {
return Err(MmapError::InvalidFormat("Invalid magic number".to_string()));
}
if header.version != VERSION {
return Err(MmapError::InvalidFormat(format!(
"Unsupported version: {}",
header.version
)));
}
Ok((header.nrows as usize, header.ncols as usize))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mmap_create_and_open() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_basic.oxiblas");
{
let mut mmat = MmapMatMut::<f64>::create(&path, 10, 10).unwrap();
for i in 0..10 {
for j in 0..10 {
mmat[(i, j)] = (i * 10 + j) as f64;
}
}
mmat.flush().unwrap();
}
{
let mmat = MmapMat::<f64>::open(&path).unwrap();
assert_eq!(mmat.shape(), (10, 10));
for i in 0..10 {
for j in 0..10 {
assert_eq!(mmat[(i, j)], (i * 10 + j) as f64);
}
}
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_views() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_views.oxiblas");
let mut mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
{
let mut view = mmat.as_mut();
for i in 0..5 {
view[(i, i)] = 1.0;
}
}
{
let view = mmat.as_ref();
for i in 0..5 {
for j in 0..5 {
if i == j {
assert_eq!(view[(i, j)], 1.0);
} else {
assert_eq!(view[(i, j)], 0.0);
}
}
}
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_f32() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_f32.oxiblas");
{
let mut mmat = MmapMatMut::<f32>::create(&path, 3, 3).unwrap();
mmat[(0, 0)] = 1.0f32;
mmat[(1, 1)] = 2.0f32;
mmat[(2, 2)] = 3.0f32;
mmat.flush().unwrap();
}
{
let mmat = MmapMat::<f32>::open(&path).unwrap();
assert_eq!(mmat[(0, 0)], 1.0f32);
assert_eq!(mmat[(1, 1)], 2.0f32);
assert_eq!(mmat[(2, 2)], 3.0f32);
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_type_mismatch() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_type_mismatch.oxiblas");
{
let _mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
}
{
let result = MmapMat::<f32>::open(&path);
assert!(result.is_err());
if let Err(MmapError::TypeMismatch { expected, found }) = result {
assert_eq!(expected, ElementType::F32);
assert_eq!(found, ElementType::F64);
} else {
panic!("Expected TypeMismatch error");
}
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_builder() {
use crate::Mat;
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_builder.oxiblas");
let mat = Mat::<f64>::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
{
let builder = MmapBuilder::<f64>::new(2, 3);
let mmat = builder.from_mat(&path, &mat.as_ref()).unwrap();
assert_eq!(mmat.shape(), (2, 3));
}
{
let mmat = MmapMat::<f64>::open(&path).unwrap();
assert_eq!(mmat[(0, 0)], 1.0);
assert_eq!(mmat[(0, 2)], 3.0);
assert_eq!(mmat[(1, 0)], 4.0);
assert_eq!(mmat[(1, 2)], 6.0);
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_read_dimensions() {
let dir = std::env::temp_dir();
let path = dir.join("test_read_dims.oxiblas");
{
let _mmat = MmapMatMut::<f64>::create(&path, 100, 200).unwrap();
}
let (nrows, ncols) = read_dimensions(&path).unwrap();
assert_eq!(nrows, 100);
assert_eq!(ncols, 200);
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_large_matrix() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_large.oxiblas");
let nrows = 1000;
let ncols = 500;
{
let mut mmat = MmapMatMut::<f64>::create(&path, nrows, ncols).unwrap();
for i in 0..nrows.min(ncols) {
mmat[(i, i)] = (i + 1) as f64;
}
mmat[(0, 0)] = -1.0;
mmat[(nrows - 1, ncols - 1)] = -2.0;
mmat.flush().unwrap();
}
{
let mmat = MmapMat::<f64>::open(&path).unwrap();
assert_eq!(mmat.shape(), (nrows, ncols));
assert_eq!(mmat[(0, 0)], -1.0);
assert_eq!(mmat[(nrows - 1, ncols - 1)], -2.0);
assert_eq!(mmat[(100, 100)], 101.0);
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_fill() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_fill.oxiblas");
{
let mut mmat = MmapMatMut::<f64>::create(&path, 5, 5).unwrap();
mmat.fill(42.0);
mmat.flush().unwrap();
}
{
let mmat = MmapMat::<f64>::open(&path).unwrap();
for i in 0..5 {
for j in 0..5 {
assert_eq!(mmat[(i, j)], 42.0);
}
}
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_write_mat() {
use crate::Mat;
let dir = std::env::temp_dir();
let path = dir.join("test_write_mat.oxiblas");
let mat = Mat::<f64>::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
write_mat(&path, &mat.as_ref()).unwrap();
let mmat = MmapMat::<f64>::open(&path).unwrap();
assert_eq!(mmat[(0, 0)], 1.0);
assert_eq!(mmat[(0, 1)], 2.0);
assert_eq!(mmat[(1, 0)], 3.0);
assert_eq!(mmat[(1, 1)], 4.0);
std::fs::remove_file(path).ok();
}
fn write_oxiblas_with_short_data(
path: &std::path::Path,
nrows: usize,
ncols: usize,
row_stride: usize,
data_bytes: usize,
) {
use std::io::Write;
let header =
Header::new::<f64>(nrows, ncols, row_stride).expect("f64 is a supported element type");
let mut file = std::fs::File::create(path).expect("create temp file");
file.write_all(&header.to_bytes()).expect("write header");
file.write_all(&vec![0u8; data_bytes])
.expect("write short data section");
file.flush().expect("flush temp file");
}
#[test]
fn test_mmap_open_truncated_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_open_truncated_ro.oxiblas");
write_oxiblas_with_short_data(&path, 100, 100, 104, 64);
match MmapMat::<f64>::open(&path) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("truncated read-only file was accepted (out-of-bounds read hazard)"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_mut_open_truncated_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_open_truncated_rw.oxiblas");
write_oxiblas_with_short_data(&path, 100, 100, 104, 64);
match MmapMatMut::<f64>::open(&path) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("truncated writable file was accepted (out-of-bounds write hazard)"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_open_bad_row_stride_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_open_bad_stride.oxiblas");
write_oxiblas_with_short_data(&path, 100, 10, 4, 4 * 10 * 8);
match MmapMat::<f64>::open(&path) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("file with row_stride < nrows was accepted"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_open_overflow_dims_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_open_overflow.oxiblas");
write_oxiblas_with_short_data(&path, usize::MAX, 1024, usize::MAX, 64);
match MmapMat::<f64>::open(&path) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("file with overflowing dimensions was accepted"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_create_overflow_dims_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_create_overflow.oxiblas");
match MmapMatMut::<f64>::create(&path, 1usize << 32, 1usize << 32) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("create() accepted overflowing dimensions"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_create_row_stride_overflow_rejected() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_create_stride_overflow.oxiblas");
match MmapMatMut::<f64>::create(&path, usize::MAX, 1) {
Err(MmapError::InvalidDimensions(_)) => {}
Err(other) => panic!("expected InvalidDimensions, got {other:?}"),
Ok(_) => panic!("create() accepted an overflowing row stride"),
}
std::fs::remove_file(path).ok();
}
#[test]
fn test_mmap_create_sane_dims_still_work() {
let dir = std::env::temp_dir();
let path = dir.join("test_mmap_create_sane.oxiblas");
{
let mut m = MmapMatMut::<f64>::create(&path, 8, 4)
.expect("creating an 8x4 matrix must succeed");
m.set(0, 0, 1.5);
m.set(7, 3, 2.5);
assert_eq!(m.get(0, 0), Some(&1.5));
assert_eq!(m.get(7, 3), Some(&2.5));
}
let reopened = MmapMat::<f64>::open(&path).expect("reopening must succeed");
assert_eq!(reopened.shape(), (8, 4));
assert_eq!(reopened.get(7, 3), Some(&2.5));
std::fs::remove_file(path).ok();
}
}