use super::error::{DiskError, DiskResult};
use super::BlockDevice;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs::File;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use tokio::sync::Mutex;
pub struct RawImage {
path: PathBuf,
size: u64,
file: Arc<Mutex<File>>,
}
impl RawImage {
pub async fn open(path: impl AsRef<Path>) -> DiskResult<Self> {
let path = path.as_ref().to_path_buf();
let file = File::open(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
DiskError::InvalidFormat {
path: path.clone(),
reason: "File not found".to_string(),
}
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
DiskError::InvalidFormat {
path: path.clone(),
reason: "Permission denied (try running as root for block devices)".to_string(),
}
} else {
DiskError::Io(e)
}
})?;
let metadata = file.metadata().await?;
let size = metadata.len();
if size == 0 {
return Err(DiskError::InvalidFormat {
path,
reason: "File is empty".to_string(),
});
}
Ok(Self {
path,
size,
file: Arc::new(Mutex::new(file)),
})
}
pub fn from_file(path: PathBuf, file: File, size: u64) -> Self {
Self {
path,
size,
file: Arc::new(Mutex::new(file)),
}
}
pub fn path(&self) -> &Path {
&self.path
}
#[cfg(target_os = "linux")]
pub async fn is_sparse(&self) -> DiskResult<bool> {
use std::os::unix::fs::MetadataExt;
let file = self.file.lock().await;
let metadata = file.metadata().await?;
let disk_usage = metadata.blocks() * 512;
Ok(disk_usage < metadata.len())
}
#[cfg(not(target_os = "linux"))]
pub async fn is_sparse(&self) -> DiskResult<bool> {
Ok(false)
}
}
impl BlockDevice for RawImage {
async fn read_at(&self, buf: &mut [u8], offset: u64) -> DiskResult<usize> {
if offset >= self.size {
return Err(DiskError::OutOfBounds {
requested: offset,
size: self.size,
});
}
let mut file = self.file.lock().await;
file.seek(std::io::SeekFrom::Start(offset)).await?;
let available = (self.size - offset) as usize;
let to_read = buf.len().min(available);
let bytes_read = file.read(&mut buf[..to_read]).await?;
Ok(bytes_read)
}
fn size(&self) -> u64 {
self.size
}
fn block_size(&self) -> u32 {
512
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn test_open_raw_image() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.img");
let mut data = vec![0u8; 1024];
data[510] = 0x55;
data[511] = 0xAA;
tokio::fs::write(&path, &data).await.unwrap();
let image = RawImage::open(&path).await.unwrap();
assert_eq!(image.size(), 1024);
}
#[tokio::test]
async fn test_read_at() {
let dir = tempdir().unwrap();
let path = dir.path().join("test.img");
let data: Vec<u8> = (0..256).collect();
tokio::fs::write(&path, &data).await.unwrap();
let image = RawImage::open(&path).await.unwrap();
let mut buf = [0u8; 10];
let read = image.read_at(&mut buf, 100).await.unwrap();
assert_eq!(read, 10);
assert_eq!(&buf, &data[100..110]);
}
#[tokio::test]
async fn test_empty_file_rejected() {
let dir = tempdir().unwrap();
let path = dir.path().join("empty.img");
tokio::fs::write(&path, b"").await.unwrap();
let result = RawImage::open(&path).await;
assert!(matches!(result, Err(DiskError::InvalidFormat { .. })));
}
#[tokio::test]
async fn test_out_of_bounds_read() {
let dir = tempdir().unwrap();
let path = dir.path().join("small.img");
tokio::fs::write(&path, b"small").await.unwrap();
let image = RawImage::open(&path).await.unwrap();
let mut buf = [0u8; 10];
let result = image.read_at(&mut buf, 1000).await;
assert!(matches!(result, Err(DiskError::OutOfBounds { .. })));
}
}