#[cfg(all(unix, feature = "libusb"))]
use anyhow::{Context, Result};
#[cfg(all(unix, feature = "libusb"))]
use memmap2::MmapMut;
#[cfg(all(unix, feature = "libusb"))]
use std::fs::OpenOptions;
#[cfg(all(unix, feature = "libusb"))]
use std::path::Path;
#[cfg(all(unix, feature = "libusb"))]
use tracing::{debug, info, trace};
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug)]
enum Backing {
Memory(Vec<u8>),
Mapped(MmapMut),
}
#[cfg(all(unix, feature = "libusb"))]
impl Backing {
fn as_slice(&self) -> &[u8] {
match self {
Backing::Memory(bytes) => bytes,
Backing::Mapped(mmap) => mmap,
}
}
fn as_mut_slice(&mut self) -> &mut [u8] {
match self {
Backing::Memory(bytes) => bytes,
Backing::Mapped(mmap) => mmap,
}
}
fn flush(&self) -> Result<()> {
match self {
Backing::Memory(_) => Ok(()),
Backing::Mapped(mmap) => mmap.flush().context("Failed to flush disk writes"),
}
}
}
#[cfg(all(unix, feature = "libusb"))]
#[derive(Debug)]
pub struct DiskImage {
backing: Backing,
total_sectors: u32,
bytes_per_sector: u32,
}
#[cfg(all(unix, feature = "libusb"))]
impl DiskImage {
pub fn in_memory(mut bytes: Vec<u8>) -> Result<Self> {
let bytes_per_sector: u32 = 512;
let rounded = bytes.len().div_ceil(bytes_per_sector as usize) * bytes_per_sector as usize;
bytes.resize(rounded, 0);
let total_sectors = u32::try_from(rounded / bytes_per_sector as usize)
.context("in-memory disk is too large to address in 32-bit sectors")?;
if total_sectors == 0 {
anyhow::bail!("in-memory disk is smaller than one 512-byte sector");
}
debug!("Created in-memory disk ({} sectors)", total_sectors);
Ok(Self {
backing: Backing::Memory(bytes),
total_sectors,
bytes_per_sector,
})
}
pub fn open_or_create(path: &Path, default_size_mb: u32) -> Result<Self> {
let bytes_per_sector: u32 = 512;
let mut options = OpenOptions::new();
options
.read(true)
.write(true)
.create(true)
.truncate(false);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options
.open(path)
.context("Failed to open/create disk image file")?;
let current_size = file.metadata()?.len();
let size_bytes = if current_size == 0 {
let size_bytes = (default_size_mb as u64) * 1024 * 1024;
file.set_len(size_bytes)
.context("Failed to set disk image size")?;
info!(
"Created disk image {} ({} MB)",
path.display(),
default_size_mb
);
size_bytes
} else {
let rounded = current_size.div_ceil(bytes_per_sector as u64) * bytes_per_sector as u64;
if rounded != current_size {
file.set_len(rounded)
.context("Failed to pad disk image to a sector boundary")?;
}
debug!(
"Opened existing disk image {} ({} bytes)",
path.display(),
rounded
);
rounded
};
let total_sectors =
u32::try_from(size_bytes / bytes_per_sector as u64).with_context(|| {
format!(
"Disk image {} is {} bytes, more than the {} sectors a 32-bit LBA can \
address",
path.display(),
size_bytes,
u32::MAX
)
})?;
if total_sectors == 0 {
anyhow::bail!(
"Disk image {} is smaller than one 512-byte sector",
path.display()
);
}
let mmap =
unsafe { MmapMut::map_mut(&file).context("Failed to memory-map disk image file")? };
Ok(Self {
backing: Backing::Mapped(mmap),
total_sectors,
bytes_per_sector,
})
}
pub fn total_sectors(&self) -> u32 {
self.total_sectors
}
pub fn bytes_per_sector(&self) -> u32 {
self.bytes_per_sector
}
pub fn read_sectors(&self, lba: u32, count: u32) -> Result<Vec<u8>> {
let end = lba
.checked_add(count)
.context("Read beyond disk bounds: LBA + count overflows")?;
if end > self.total_sectors {
anyhow::bail!(
"Read beyond disk bounds: LBA {} + {} > {}",
lba,
count,
self.total_sectors
);
}
let offset = lba as usize * self.bytes_per_sector as usize;
let length = count as usize * self.bytes_per_sector as usize;
trace!(
"Reading {} sectors from LBA {} (offset {}, length {})",
count,
lba,
offset,
length
);
Ok(self.backing.as_slice()[offset..offset + length].to_vec())
}
pub fn write_sectors(&mut self, lba: u32, data: &[u8]) -> Result<u32> {
let count = u32::try_from(data.len().div_ceil(self.bytes_per_sector as usize))
.context("Write payload is too large to address in sectors")?;
let end = lba
.checked_add(count)
.context("Write beyond disk bounds: LBA + count overflows")?;
if end > self.total_sectors {
anyhow::bail!(
"Write beyond disk bounds: LBA {} + {} > {}",
lba,
count,
self.total_sectors
);
}
let offset = lba as usize * self.bytes_per_sector as usize;
let length = data.len();
trace!(
"Writing {} bytes ({} sectors) to LBA {} (offset {})",
length,
count,
lba,
offset
);
self.backing.as_mut_slice()[offset..offset + length].copy_from_slice(data);
self.backing.flush()?;
Ok(count)
}
#[allow(dead_code)]
pub fn zero_sectors(&mut self, lba: u32, count: u32) -> Result<()> {
let end = lba
.checked_add(count)
.context("Zero beyond disk bounds: LBA + count overflows")?;
if end > self.total_sectors {
anyhow::bail!(
"Zero beyond disk bounds: LBA {} + {} > {}",
lba,
count,
self.total_sectors
);
}
let offset = lba as usize * self.bytes_per_sector as usize;
let length = count as usize * self.bytes_per_sector as usize;
debug!("Zeroing {} sectors from LBA {}", count, lba);
self.backing.as_mut_slice()[offset..offset + length].fill(0);
self.backing.flush()?;
Ok(())
}
}