//! Guided and unattended installation of complete disk images on Linux. pub mod cli; pub mod device; mod image; use fds_common::{Error, Result}; use serde::Serialize; use std::{ fs::File, os::{fd::AsRawFd, unix::fs::MetadataExt}, path::{Path, PathBuf}, }; #[derive(Serialize)] pub struct Plan { pub image_path: PathBuf, pub image: image::Geometry, pub sha256: String, pub target: device::Target, pub target_id: String, pub confirmation: String, } pub struct Prepared { pub plan: Plan, source: File, } pub fn prepare( image_path: &Path, device_path: &Path, file_target: bool, expected_sha256: Option<&str>, ) -> Result { let image_path = image_path.canonicalize()?; let source = crate::cartridge::open_image(&image_path)?; let geometry = image::inspect(&source, source.metadata()?.len())?; let sha256 = fds_burn::image::digest(&source, geometry.bytes, |_| Ok(()))?; if expected_sha256.is_some_and(|expected| expected != sha256) { return Err(Error( "Image SHA-256 does not match --sha256; target untouched".into(), )); } if image::inspect(&source, source.metadata()?.len())? != geometry { return Err(Error("Image geometry changed during inspection".into())); } let target = device::inspect(device_path, file_target)?; let metadata = std::fs::metadata(target.path())?; if source.metadata()?.dev() == metadata.dev() && source.metadata()?.ino() == metadata.ino() { return Err(Error("Image and target refer to the same file".into())); } target.protect()?; if target.bytes() < geometry.bytes || target.bytes() % 512 != 0 { return Err(Error( "Destination is smaller than the image or is not sector aligned".into(), )); } let target_id = target.id()?; let confirmation = format!("ERASE {}", target.path().display()); Ok(Prepared { plan: Plan { image_path, image: geometry, sha256, target, target_id, confirmation, }, source, }) } impl Prepared { pub fn write(&self) -> Result<()> { let target = self.plan.target.open()?; let source_meta = self.source.metadata()?; let target_meta = target.metadata()?; if source_meta.dev() == target_meta.dev() && source_meta.ino() == target_meta.ino() { return Err(Error("Image and destination are the same open file".into())); } image::transfer( &self.source, &target, &self.plan.image, &self.plan.sha256, self.plan.target.bytes(), |phase, bytes| { self.plan.target.present()?; eprintln!("{phase}: {bytes}/{} bytes", self.plan.image.bytes); Ok(()) }, )?; self.plan.target.present()?; if !self.plan.target.is_file() && unsafe { libc::ioctl(target.as_raw_fd(), 0x125f as libc::Ioctl) } < 0 { return Err(Error(format!( "Image verified, but the kernel could not reload the partition table: {}. Do not use the disk until it is reconnected and inspected", std::io::Error::last_os_error() ))); } Ok(()) } }