use anyhow::Result;
use openssl::rand::rand_bytes;
use crate::pipeline::Runner;
const PART_SIZE_UNITS: &[(&str, u64)] = &[
("K", 1024),
("M", 1024 * 1024),
("G", 1024 * 1024 * 1024),
("T", 1024 * 1024 * 1024 * 1024),
("P", 1024 * 1024 * 1024 * 1024 * 1024),
];
fn part_size_bytes(size: &str) -> Result<u64> {
let s = size.strip_prefix('+').unwrap_or(size);
let (num_str, unit) = s
.split_at_checked(s.len().saturating_sub(1))
.ok_or_else(|| anyhow::anyhow!("unsupported partition size: {size}"))?;
let num: u64 = num_str
.parse()
.map_err(|_| anyhow::anyhow!("unsupported partition size: {size}"))?;
let multiplier = PART_SIZE_UNITS
.iter()
.find(|(u, _)| *u == unit)
.map(|(_, m)| *m)
.ok_or_else(|| anyhow::anyhow!("unsupported partition unit: {unit}"))?;
num.checked_mul(multiplier)
.ok_or_else(|| anyhow::anyhow!("partition size is too large: {size}"))
}
const PART_TYPE_ESP: [u8; 16] = [
0x28, 0x73, 0x2A, 0xC1, 0x1F, 0xF8, 0xD2, 0x11, 0xBA, 0x4B, 0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B,
];
const PART_TYPE_LINUX: [u8; 16] = [
0xAF, 0x3D, 0xC6, 0x0F, 0x83, 0x84, 0x72, 0x47, 0x8E, 0x79, 0x3D, 0x69, 0xD8, 0x47, 0x7D, 0xE4,
];
pub fn partition_drive(
runner: &mut Runner,
drive: &str,
efi_size: &str,
home_size: &str,
) -> Result<PartitionLayout> {
let efi_bytes = part_size_bytes(efi_size)?;
let home_bytes = part_size_bytes(home_size)?;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(drive)?;
let sector_size = gptman::linux::get_sector_size(&mut file).unwrap_or(512);
let mut disk_guid = [0u8; 16];
rand_bytes(&mut disk_guid)?;
let mut gpt = gptman::GPT::new_from(&mut file, sector_size, disk_guid)?;
let efi_sectors = efi_bytes.div_ceil(sector_size);
let home_sectors = home_bytes.div_ceil(sector_size);
let efi_start = gpt
.find_first_place(efi_sectors)
.ok_or_else(|| anyhow::anyhow!("no room for EFI partition on {drive}"))?;
let mut efi_guid = [0u8; 16];
rand_bytes(&mut efi_guid)?;
gpt[1] = gptman::GPTPartitionEntry {
partition_type_guid: PART_TYPE_ESP,
unique_partition_guid: efi_guid,
starting_lba: efi_start,
ending_lba: efi_start + efi_sectors - 1,
attribute_bits: 0,
partition_name: "EFI".into(),
};
let home_start = gpt
.find_last_place(home_sectors)
.ok_or_else(|| anyhow::anyhow!("no room for home partition on {drive}"))?;
let last_usable = gpt.header.last_usable_lba;
let mut home_guid = [0u8; 16];
rand_bytes(&mut home_guid)?;
gpt[3] = gptman::GPTPartitionEntry {
partition_type_guid: PART_TYPE_LINUX,
unique_partition_guid: home_guid,
starting_lba: home_start,
ending_lba: last_usable,
attribute_bits: 0,
partition_name: "home".into(),
};
let root_size = gpt.get_maximum_partition_size()?;
let root_start = gpt
.find_optimal_place(root_size)
.ok_or_else(|| anyhow::anyhow!("no room for root partition on {drive}"))?;
let mut root_guid = [0u8; 16];
rand_bytes(&mut root_guid)?;
gpt[2] = gptman::GPTPartitionEntry {
partition_type_guid: PART_TYPE_LINUX,
unique_partition_guid: root_guid,
starting_lba: root_start,
ending_lba: root_start + root_size - 1,
attribute_bits: 0,
partition_name: "root".into(),
};
let part_bytes = |n: u32| (gpt[n].ending_lba - gpt[n].starting_lba + 1) * sector_size;
runner.log(&format!(
"wrote GPT to {drive}: EFI {}, root {}, home {}",
crate::util::human_size(part_bytes(1)),
crate::util::human_size(part_bytes(2)),
crate::util::human_size(part_bytes(3)),
));
gptman::GPT::write_protective_mbr_into(&mut file, sector_size)?;
gpt.write_into(&mut file)?;
file.sync_all()?;
gptman::linux::reread_partition_table(&mut file)
.map_err(|e| anyhow::anyhow!("failed to reread partition table on {drive}: {e}"))?;
Ok(PartitionLayout {
efi: PartitionInfo {
number: 1,
unique_guid: gpt[1].unique_partition_guid,
starting_lba: gpt[1].starting_lba,
ending_lba: gpt[1].ending_lba,
},
})
}
fn partition_info(number: u32, entry: &gptman::GPTPartitionEntry) -> PartitionInfo {
PartitionInfo {
number,
unique_guid: entry.unique_partition_guid,
starting_lba: entry.starting_lba,
ending_lba: entry.ending_lba,
}
}
pub fn load_partition_layout(drive: &str) -> Result<PartitionLayout> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.open(drive)
.map_err(|e| anyhow::anyhow!("failed to open {drive}: {e}"))?;
let gpt = gptman::GPT::find_from(&mut file)
.map_err(|e| anyhow::anyhow!("failed to read GPT from {drive}: {e}"))?;
for number in 1..=gpt.header.number_of_partition_entries {
let entry = &gpt[number];
if entry.is_used() && entry.partition_type_guid == PART_TYPE_ESP {
return Ok(PartitionLayout {
efi: partition_info(number, entry),
});
}
}
Err(anyhow::anyhow!("no EFI partition found on {drive}"))
}
pub fn min_target_size_bytes(config: &crate::config::Config) -> Result<u64> {
let efi = format!("+{}", config.partitions.efi);
let root = format!("+{}", config.partitions.root_min);
let home = format!("+{}", config.partitions.home);
let sizes = [efi.as_str(), root.as_str(), home.as_str(), "+1G"];
sizes.iter().try_fold(0u64, |total, size| {
let bytes = part_size_bytes(size)?;
total
.checked_add(bytes)
.ok_or_else(|| anyhow::anyhow!("minimum target size is too large"))
})
}
pub fn derive_partitions(drive: &str) -> DerivedPartitions {
let sep = if drive.ends_with(|c: char| c.is_ascii_digit()) {
"p"
} else {
""
};
DerivedPartitions {
efi_part: format!("{drive}{sep}1"),
root_part: format!("{drive}{sep}2"),
home_part: format!("{drive}{sep}3"),
..Default::default()
}
}
#[derive(Debug, Clone)]
pub struct PartitionInfo {
pub number: u32,
pub unique_guid: [u8; 16],
pub starting_lba: u64,
pub ending_lba: u64,
}
#[derive(Debug, Clone)]
pub struct PartitionLayout {
pub efi: PartitionInfo,
}
#[derive(Debug, Clone, Default)]
pub struct DerivedPartitions {
pub efi_part: String,
pub efi_uuid: String,
pub root_part: String,
pub root_uuid: String,
pub home_part: String,
pub home_uuid: String,
pub layout: Option<PartitionLayout>,
}