use std::{fs::File, io::Write, mem::ManuallyDrop, path::PathBuf, process::Command};
use crate::context::DADKExecContext;
use anyhow::{anyhow, Result};
use dadk_config::rootfs::{fstype::FsType, partition::PartitionType};
use super::loopdev::LoopDeviceBuilder;
pub(super) fn create(ctx: &DADKExecContext, skip_if_exists: bool) -> Result<()> {
let disk_image_path = ctx.disk_image_path();
if disk_image_path.exists() {
if skip_if_exists {
return Ok(());
}
return Err(anyhow!(
"Disk image already exists: {}",
disk_image_path.display()
));
}
disk_path_safety_check(&disk_image_path)?;
let image_size = ctx.disk_image_size();
create_raw_img(&disk_image_path, image_size).expect("Failed to create raw disk image");
let r = if ctx.rootfs().partition.image_should_be_partitioned() {
create_partitioned_image(ctx, &disk_image_path)
} else {
create_unpartitioned_image(ctx, &disk_image_path)
};
if r.is_err() {
std::fs::remove_file(&disk_image_path).expect("Failed to remove disk image");
}
r
}
pub(super) fn delete(ctx: &DADKExecContext, skip_if_not_exists: bool) -> Result<()> {
let disk_image_path = ctx.disk_image_path();
if !disk_image_path.exists() {
if skip_if_not_exists {
return Ok(());
}
return Err(anyhow!(
"Disk image does not exist: {}",
disk_image_path.display()
));
}
disk_path_safety_check(&disk_image_path)?;
std::fs::remove_file(&disk_image_path)
.map_err(|e| anyhow!("Failed to remove disk image: {}", e))?;
Ok(())
}
pub fn mount(ctx: &DADKExecContext) -> Result<()> {
let disk_image_path = ctx.disk_image_path();
if !disk_image_path.exists() {
return Err(anyhow!(
"Disk image does not exist: {}",
disk_image_path.display()
));
}
let disk_mount_path = ctx.disk_mount_path();
std::fs::create_dir_all(&disk_mount_path)
.map_err(|e| anyhow!("Failed to create disk mount path: {}", e))?;
let partitioned = ctx.rootfs().partition.image_should_be_partitioned();
log::trace!("Disk image is partitioned: {}", partitioned);
if partitioned {
mount_partitioned_image(ctx, &disk_image_path, &disk_mount_path)?
} else {
mount_unpartitioned_image(ctx, &disk_image_path, &disk_mount_path)?
}
log::info!("Disk image mounted at {}", disk_mount_path.display());
Ok(())
}
fn mount_partitioned_image(
ctx: &DADKExecContext,
disk_image_path: &PathBuf,
disk_mount_path: &PathBuf,
) -> Result<()> {
let mut loop_device = ManuallyDrop::new(
LoopDeviceBuilder::new()
.img_path(disk_image_path.clone())
.build()
.map_err(|e| anyhow!("Failed to create loop device: {}", e))?,
);
loop_device
.attach()
.map_err(|e| anyhow!("Failed to attach loop device: {}", e))?;
let dev_path = loop_device.partition_path(1)?;
mount_unpartitioned_image(ctx, &dev_path, disk_mount_path)?;
Ok(())
}
fn mount_unpartitioned_image(
_ctx: &DADKExecContext,
disk_image_path: &PathBuf,
disk_mount_path: &PathBuf,
) -> Result<()> {
let cmd = Command::new("mount")
.arg(disk_image_path)
.arg(disk_mount_path)
.output()
.map_err(|e| anyhow!("Failed to mount disk image: {}", e))?;
if !cmd.status.success() {
return Err(anyhow!(
"Failed to mount disk image: {}",
String::from_utf8_lossy(&cmd.stderr)
));
}
Ok(())
}
pub fn umount(ctx: &DADKExecContext) -> Result<()> {
let disk_img_path = ctx.disk_image_path();
let disk_mount_path = ctx.disk_mount_path();
let mut loop_device = LoopDeviceBuilder::new().img_path(disk_img_path).build();
let should_detach_loop_device: bool;
if let Ok(loop_device) = loop_device.as_mut() {
if let Err(e) = loop_device.attach_by_exists() {
log::trace!("umount: Failed to attach loop device: {}", e);
}
should_detach_loop_device = loop_device.attached();
} else {
should_detach_loop_device = false;
}
if disk_mount_path.exists() {
let cmd = Command::new("umount")
.arg(disk_mount_path)
.output()
.map_err(|e| anyhow!("Failed to umount disk image: {}", e));
match cmd {
Ok(cmd) => {
if !cmd.status.success() {
let e = anyhow!(
"Failed to umount disk image: {}",
String::from_utf8_lossy(&cmd.stderr)
);
if should_detach_loop_device {
log::error!("{}", e);
} else {
return Err(e);
}
}
}
Err(e) => {
if should_detach_loop_device {
log::error!("{}", e);
} else {
return Err(e);
}
}
}
}
if let Ok(mut loop_device) = loop_device {
let loop_dev_path = loop_device.dev_path().cloned();
loop_device.detach().ok();
log::info!("Loop device detached: {:?}", loop_dev_path);
}
Ok(())
}
fn disk_path_safety_check(disk_image_path: &PathBuf) -> Result<()> {
const DONT_ALLOWED_PREFIX: [&str; 5] =
["/dev/sd", "/dev/hd", "/dev/vd", "/dev/nvme", "/dev/mmcblk"];
let path = disk_image_path.to_str().ok_or(anyhow!(
"disk path safety check failed: disk path is not valid utf-8"
))?;
DONT_ALLOWED_PREFIX.iter().for_each(|prefix| {
if path.starts_with(prefix) {
panic!("disk path safety check failed: disk path is not allowed to be a device node(except loop dev)");
}
});
Ok(())
}
fn create_partitioned_image(ctx: &DADKExecContext, disk_image_path: &PathBuf) -> Result<()> {
let part_type = ctx.rootfs().partition.partition_type;
DiskPartitioner::create_partitioned_image(disk_image_path, part_type)?;
let mut loop_device = LoopDeviceBuilder::new()
.img_path(disk_image_path.clone())
.build()
.map_err(|e| anyhow!("Failed to create loop device: {}", e))?;
loop_device
.attach()
.map_err(|e| anyhow!("Failed to attach loop device: {}", e))?;
let partition_path = loop_device.partition_path(1)?;
let fs_type = ctx.rootfs().metadata.fs_type;
DiskFormatter::format_disk(&partition_path, &fs_type)?;
loop_device.detach()?;
Ok(())
}
fn create_unpartitioned_image(ctx: &DADKExecContext, disk_image_path: &PathBuf) -> Result<()> {
let fs_type = ctx.rootfs().metadata.fs_type;
DiskFormatter::format_disk(disk_image_path, &fs_type)
}
fn create_raw_img(disk_image_path: &PathBuf, image_size: usize) -> Result<()> {
log::trace!("Creating raw disk image: {}", disk_image_path.display());
if let Some(parent) = disk_image_path.parent() {
log::trace!("Creating parent directory: {}", parent.display());
std::fs::create_dir_all(parent)?;
}
let mut file = File::create(disk_image_path)?;
file.set_len(image_size.try_into().unwrap())?;
let zero_buffer = vec![0u8; 4096]; let mut remaining_size = image_size;
while remaining_size > 0 {
let write_size = std::cmp::min(remaining_size, zero_buffer.len());
file.write_all(&zero_buffer[..write_size as usize])?;
remaining_size -= write_size;
}
Ok(())
}
pub fn check_disk_image_exists(ctx: &DADKExecContext) -> Result<()> {
let disk_image_path = ctx.disk_image_path();
if disk_image_path.exists() {
println!("1");
} else {
println!("0");
}
Ok(())
}
pub fn show_mount_point(ctx: &DADKExecContext) -> Result<()> {
let disk_mount_path = ctx.disk_mount_path();
println!("{}", disk_mount_path.display());
Ok(())
}
pub fn show_loop_device(ctx: &DADKExecContext) -> Result<()> {
let disk_image_path = ctx.disk_image_path();
let mut loop_device = LoopDeviceBuilder::new().img_path(disk_image_path).build()?;
if let Err(e) = loop_device.attach_by_exists() {
log::error!("Failed to attach loop device: {}", e);
} else {
println!("{}", loop_device.dev_path().unwrap());
}
Ok(())
}
struct DiskPartitioner;
impl DiskPartitioner {
fn create_partitioned_image(disk_image_path: &PathBuf, part_type: PartitionType) -> Result<()> {
match part_type {
PartitionType::None => {
return Err(anyhow::anyhow!("Invalid partition type: None"));
}
PartitionType::Mbr => {
Self::create_mbr_partitioned_image(disk_image_path)?;
}
PartitionType::Gpt => {
Self::create_gpt_partitioned_image(disk_image_path)?;
}
}
Ok(())
}
fn create_mbr_partitioned_image(disk_image_path: &PathBuf) -> Result<()> {
let disk_image_path_str = disk_image_path.to_str().expect("Invalid path");
let output = Command::new("fdisk")
.arg("--help")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()?
.wait_with_output()?;
if !output.status.success() {
return Err(anyhow::anyhow!("Command fdisk not found"));
}
let fdisk_commands = "o\nn\n\n\n\n\na\nw\n";
let mut fdisk_child = Command::new("fdisk")
.arg(disk_image_path_str)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()?;
let fdisk_stdin = fdisk_child.stdin.as_mut().expect("Failed to open stdin");
fdisk_stdin.write_all(fdisk_commands.as_bytes())?;
fdisk_stdin.flush()?;
fdisk_child
.wait()
.unwrap_or_else(|e| panic!("Failed to run fdisk: {}", e));
Ok(())
}
fn create_gpt_partitioned_image(_disk_image_path: &PathBuf) -> Result<()> {
unimplemented!("Not implemented: create_gpt_partitioned_image");
}
}
struct DiskFormatter;
impl DiskFormatter {
fn format_disk(disk_image_path: &PathBuf, fs_type: &FsType) -> Result<()> {
match fs_type {
FsType::Fat32 => Self::format_fat32(disk_image_path),
}
}
fn format_fat32(disk_image_path: &PathBuf) -> Result<()> {
let status = Command::new("mkfs.fat")
.arg("-F32")
.arg(disk_image_path.to_str().unwrap())
.status()?;
if status.success() {
Ok(())
} else {
Err(anyhow::anyhow!("Failed to format disk image as FAT32"))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Read;
use tempfile::NamedTempFile;
#[test]
fn test_create_raw_img_functional() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let disk_image_path = temp_file.path().to_path_buf();
let disk_image_size = 1024 * 1024usize;
create_raw_img(&disk_image_path, disk_image_size)?;
let metadata = fs::metadata(&disk_image_path)?;
assert_eq!(metadata.len(), disk_image_size as u64);
let mut file = File::open(&disk_image_path)?;
let mut buffer = vec![0u8; 4096];
let mut all_zeros = true;
while file.read(&mut buffer)? > 0 {
for byte in &buffer {
if *byte != 0 {
all_zeros = false;
break;
}
}
}
assert!(all_zeros, "File content is not all zeros");
Ok(())
}
#[test]
fn test_format_fat32() {
let temp_file = NamedTempFile::new().expect("Failed to create temp file");
let disk_image_path = temp_file.path().to_path_buf();
let image_size = 16 * 1024 * 1024usize;
create_raw_img(&disk_image_path, image_size).expect("Failed to create raw disk image");
DiskFormatter::format_disk(&disk_image_path, &FsType::Fat32)
.expect("Failed to format disk image as FAT32");
let output = Command::new("file")
.arg("-sL")
.arg(&disk_image_path)
.output()
.expect("Failed to execute 'file' command");
let output_str = String::from_utf8_lossy(&output.stdout);
assert!(
output_str.contains("FAT (32 bit)"),
"Disk image is not formatted as FAT32"
);
}
#[test]
fn test_create_mbr_partitioned_image() -> Result<()> {
let temp_file = NamedTempFile::new()?;
let disk_image_path = temp_file.path().to_path_buf();
eprintln!("Disk image path: {:?}", disk_image_path);
let disk_image_size = 16 * 1024 * 1024usize; create_raw_img(&disk_image_path, disk_image_size)?;
DiskPartitioner::create_mbr_partitioned_image(&disk_image_path)?;
let output = Command::new("fdisk")
.env("LANG", "C") .env("LC_ALL", "C") .arg("-l")
.arg(&disk_image_path)
.output()
.expect("Failed to execute 'fdisk -l' command");
let output_str = String::from_utf8_lossy(&output.stdout);
assert!(
output_str.contains("Disklabel type: dos"),
"Disk image does not have an MBR partition table"
);
assert!(
output_str.contains("Start"),
"Disk image does not have a partition"
);
Ok(())
}
}