use libc::fallocate; use serde::{Deserialize, Deserializer}; use std::{ fs::OpenOptions, io, os::unix::io::AsRawFd, path::Path, process::Command, sync::{Arc, Mutex, MutexGuard}, }; use tempfile::{NamedTempFile, TempPath}; // All tests use the same loopback device interface and so can tread on each others toes leading to // racy tests. So we need to lock all tests to ensure only one runs at a time. lazy_static::lazy_static! { static ref LOCK: Arc> = Arc::new(Mutex::new(())); } pub fn create_backing_file(size: i64) -> TempPath { let file = NamedTempFile::new().expect("should be able to create a temp file"); assert!( unsafe { fallocate(file.as_raw_fd(), 0, 0, size) } >= 0, "should be able to allocate the temp file: {}", io::Error::last_os_error() ); file.into_temp_path() } pub fn partition_backing_file(backing_file: impl AsRef, size: u64) { gpt::mbr::ProtectiveMBR::new() .overwrite_lba0(&mut OpenOptions::new().write(true).open(&backing_file).unwrap()) .expect("failed to write MBR"); let mut disk = gpt::GptConfig::new() .initialized(false) .writable(true) .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) .open(backing_file) .expect("could not open backing file"); disk.update_partitions(std::collections::BTreeMap::::new()) .expect("coult not initialize blank partition table"); disk.add_partition( "Linux filesystem", size, gpt::partition_types::LINUX_FS, 0, None, ) .expect("could not create partition"); disk.write() .expect("could not write partition table to backing file"); } pub fn setup() -> MutexGuard<'static, ()> { let lock = LOCK.lock().unwrap(); detach_all(); lock } pub fn attach_file(loop_dev: &str, backing_file: &str, offset: u64, sizelimit: u64) { if !Command::new("losetup") .args([ loop_dev, backing_file, "--offset", &offset.to_string(), "--sizelimit", &sizelimit.to_string(), ]) .status() .expect("failed to attach backing file to loop device") .success() { panic!("failed to cleanup existing loop devices") } } pub fn detach_all() { std::thread::sleep(std::time::Duration::from_millis(10)); if !Command::new("losetup") .args(["-D"]) .status() .expect("failed to cleanup existing loop devices") .success() { panic!("failed to cleanup existing loop devices") } std::thread::sleep(std::time::Duration::from_millis(10)); } pub fn list_device(dev_file: Option<&str>) -> Vec { let mut output = Command::new("losetup"); output.args(["-J", "-l"]); if let Some(dev_file) = dev_file { output.arg(dev_file); } let output = output .output() .expect("failed to cleanup existing loop devices"); if output.stdout.is_empty() { Vec::new() } else { serde_json::from_slice::(&output.stdout) .unwrap() .loopdevices } } #[derive(Deserialize, Debug)] pub struct LoopDeviceOutput { pub name: String, #[serde(rename = "sizelimit")] #[serde(deserialize_with = "deserialize_optional_number_from_string")] pub size_limit: Option, #[serde(deserialize_with = "deserialize_optional_number_from_string")] pub offset: Option, #[serde(rename = "back-file")] //#[serde(deserialize_with = "deserialize_nullable_string")] pub back_file: Option, } #[derive(Deserialize, Debug)] pub struct ListOutput { pub loopdevices: Vec, } pub fn deserialize_optional_number_from_string<'de, D>( deserializer: D, ) -> Result, D::Error> where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum StringOrInt { String(Option), Number(Option), } match StringOrInt::deserialize(deserializer)? { StringOrInt::String(None) | StringOrInt::Number(None) => Ok(None), StringOrInt::String(Some(s)) => Ok(Some(s.parse().map_err(serde::de::Error::custom)?)), StringOrInt::Number(Some(i)) => Ok(Some(i)), } }