use alloc::boxed::Box;
use alloc::string::{String, ToString};
use core::fmt;
use std::path::{Path, PathBuf};
use fstool::block::{BlockDevice, CreateOpts, FileBackend, Qcow2Backend};
use crate::core::error::{BusError, Result};
use crate::core::space::MemResult;
use crate::core::sync::{LockRank, Mutex};
use crate::dev::ata::{Medium, Snapshot};
use super::{config_error, media_error};
#[derive(Debug, Clone)]
pub struct ImageOptions {
pub read_only: bool,
pub snapshot: Snapshot,
pub create: Option<u64>,
pub password: Option<String>,
pub cluster: u32,
}
impl Default for ImageOptions {
fn default() -> ImageOptions {
ImageOptions {
read_only: false,
snapshot: Snapshot::Reference,
create: None,
password: None,
cluster: 0,
}
}
}
impl ImageOptions {
#[must_use]
pub fn new() -> ImageOptions {
ImageOptions::default()
}
#[must_use]
pub fn read_only(mut self, yes: bool) -> ImageOptions {
self.read_only = yes;
self
}
#[must_use]
pub fn snapshot(mut self, policy: Snapshot) -> ImageOptions {
self.snapshot = policy;
self
}
#[must_use]
pub fn create(mut self, bytes: u64) -> ImageOptions {
self.create = Some(bytes);
self
}
#[must_use]
pub fn password(mut self, password: impl Into<String>) -> ImageOptions {
self.password = Some(password.into());
self
}
#[must_use]
pub fn cluster(mut self, bytes: u32) -> ImageOptions {
self.cluster = bytes;
self
}
}
pub struct Image {
describe: String,
capacity: u64,
read_only: bool,
policy: Snapshot,
device: Mutex<Box<dyn BlockDevice>>,
}
impl fmt::Debug for Image {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Image")
.field("describe", &self.describe)
.field("capacity", &self.capacity)
.field("read_only", &self.read_only)
.field("snapshot", &self.policy)
.finish_non_exhaustive()
}
}
impl Image {
pub fn open(path: &Path, opts: &ImageOptions) -> Result<Image> {
let shown = path.display().to_string();
let device = open_device(path, opts).map_err(|e| config_error(&shown, &e))?;
let resolved = std::fs::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path));
let describe = alloc::format!(
"{} {} {}",
format_of(path),
resolved.display(),
device.total_size()
);
Image::wrap(describe, device, opts)
}
pub fn from_device(
describe: impl Into<String>,
device: Box<dyn BlockDevice>,
opts: &ImageOptions,
) -> Result<Image> {
Image::wrap(describe.into(), device, opts)
}
fn wrap(describe: String, device: Box<dyn BlockDevice>, opts: &ImageOptions) -> Result<Image> {
let capacity = device.total_size();
if capacity == 0 {
return Err(config_error(
&describe,
&fstool::Error::InvalidArgument(
"the image is empty; a drive holds at least one sector".to_string(),
),
));
}
if !capacity.is_multiple_of(crate::dev::ata::disk::SECTOR) {
return Err(config_error(
&describe,
&fstool::Error::InvalidArgument(alloc::format!(
"{capacity} bytes is not a whole number of 512-byte sectors"
)),
));
}
Ok(Image {
describe,
capacity,
read_only: opts.read_only,
policy: opts.snapshot,
device: Mutex::with_rank(LockRank::LEAF, device),
})
}
#[must_use]
pub fn describe(&self) -> &str {
&self.describe
}
#[must_use]
pub fn block_size(&self) -> u32 {
self.device.lock().block_size()
}
fn bounds(&self, offset: u64, len: u64) -> MemResult {
let end = offset.checked_add(len).ok_or(BusError::BadAccess)?;
if end > self.capacity {
return Err(BusError::BadAccess);
}
Ok(())
}
}
impl Medium for Image {
fn capacity(&self) -> u64 {
self.capacity
}
fn read_at(&self, offset: u64, dst: &mut [u8]) -> MemResult {
self.bounds(offset, dst.len() as u64)?;
if dst.is_empty() {
return Ok(());
}
self.device
.lock()
.read_at(offset, dst)
.map_err(|e| media_error(&e))
}
fn write_at(&self, offset: u64, src: &[u8]) -> MemResult {
if self.read_only {
return Err(BusError::Protected);
}
self.bounds(offset, src.len() as u64)?;
if src.is_empty() {
return Ok(());
}
self.device
.lock()
.write_at(offset, src)
.map_err(|e| media_error(&e))
}
fn flush(&self) -> MemResult {
if self.read_only {
return Ok(());
}
self.device.lock().sync().map_err(|e| media_error(&e))
}
fn is_read_only(&self) -> bool {
self.read_only
}
fn snapshot(&self) -> Snapshot {
self.policy
}
fn describe(&self) -> String {
self.describe.clone()
}
}
fn open_device(path: &Path, opts: &ImageOptions) -> fstool::Result<Box<dyn BlockDevice>> {
if let Some(size) = opts.create {
if opts.read_only {
return Err(fstool::Error::InvalidArgument(
"an image cannot be both created and read-only".to_string(),
));
}
let mut create = CreateOpts::default();
if opts.cluster != 0 {
if !opts.cluster.is_power_of_two() || opts.cluster < 512 {
return Err(fstool::Error::InvalidArgument(alloc::format!(
"a qcow2 cluster is a power of two of at least 512 bytes, not {}",
opts.cluster
)));
}
create.cluster_size = opts.cluster;
}
return fstool::block::create_image(path, size, &create);
}
let password = opts.password.as_deref();
if opts.read_only {
fstool::block::open_image_read_only_with_password(path, password)
} else {
fstool::block::open_image_with_password(path, password)
}
}
fn format_of(path: &Path) -> &'static str {
if Qcow2Backend::probe(path).unwrap_or(false) {
return "qcow2";
}
if fstool::block::dmg::probe(path).unwrap_or(false) {
return "dmg";
}
if fstool::block::diskcopy::probe(path).unwrap_or(false) {
return "diskcopy42";
}
"raw"
}
pub fn create_raw(path: &Path, bytes: u64) -> Result<()> {
FileBackend::create(path, bytes)
.map(|_| ())
.map_err(|e| config_error(&path.display().to_string(), &e))
}
#[cfg(test)]
mod tests;