| // Copyright 2023 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| use std::{ | |
| ffi::{CStr, CString}, | |
| marker::PhantomData, | |
| os::unix::prelude::OsStrExt, | |
| path::Path, | |
| ptr::null, | |
| }; | |
| use anyhow::{bail, Context, Result}; | |
| use gpt_disk_types::{guid, LbaRangeInclusive}; | |
| use rand::random; | |
| use uguid::Guid; | |
| use vboot_reference_sys::vboot_host; | |
| /// Partition type for the thirteenth partition we are adding. We are marking | |
| /// the partition as CHROMEOS_FUTURE_USE type. | |
| const CHROMEOS_FUTURE_USE_PART_TYPE: Guid = guid!("2e0a753d-9e48-43b0-8337-b15192cb1b5e"); | |
| /// Adds a new GPT partition with the given arguments. The new partition has the | |
| /// type GUID `CHROMEOS_FUTURE_USE_PART_TYPE`. | |
| pub fn add_cgpt_partition( | |
| index: u32, | |
| disk_path: &Path, | |
| label: &str, | |
| range: LbaRangeInclusive, | |
| ) -> Result<()> { | |
| let begin = range.start().to_u64(); | |
| let size = range.num_blocks(); | |
| let disk_path = CString::new(disk_path.as_os_str().as_bytes()) | |
| .context("unable to convert drive path to CString")?; | |
| let label = CString::new(label).context("unable to convert label to CString")?; | |
| let mut params = get_cgpt_add_params( | |
| index, | |
| disk_path.as_c_str(), | |
| Some(label.as_c_str()), | |
| Some(begin), | |
| Some(size), | |
| Some(CHROMEOS_FUTURE_USE_PART_TYPE), | |
| ); | |
| params.cgpt_add() | |
| } | |
| /// Removes a partition at `index`. | |
| pub fn remove_cgpt_partition(index: u32, disk_path: &Path) -> Result<()> { | |
| let disk_path = CString::new(disk_path.as_os_str().as_bytes()) | |
| .context("unable to convert drive path to CString")?; | |
| let mut params = get_cgpt_add_params( | |
| index, | |
| disk_path.as_c_str(), | |
| /*label=*/ None, | |
| /*begin=*/ None, | |
| /*size=*/ None, | |
| Some(Guid::ZERO), | |
| ); | |
| params.cgpt_add() | |
| } | |
| /// Resizes an existing partition at `index`. | |
| pub fn resize_cgpt_partition( | |
| index: u32, | |
| disk_path: &Path, | |
| label: &str, | |
| new_range: LbaRangeInclusive, | |
| ) -> Result<()> { | |
| let begin = new_range.start().to_u64(); | |
| let size = new_range.num_blocks(); | |
| let disk_path = CString::new(disk_path.as_os_str().as_bytes()) | |
| .context("unable to convert drive path to CString")?; | |
| let label = CString::new(label).context("unable to convert label to CString")?; | |
| let mut params = get_cgpt_add_params( | |
| index, | |
| disk_path.as_c_str(), | |
| Some(label.as_c_str()), | |
| Some(begin), | |
| Some(size), | |
| /*type_guid=*/ None, | |
| ); | |
| params.cgpt_add() | |
| } | |
| /// Wrapper that handles lifetimes of the members of | |
| /// [`vboot_host::CgptAddParams`], which holds pointers to [`CString`]. | |
| struct CgptAddParamsWrapper<'a> { | |
| params: vboot_host::CgptAddParams, | |
| phantom_data: PhantomData<&'a ()>, | |
| } | |
| impl CgptAddParamsWrapper<'_> { | |
| pub fn cgpt_add(&mut self) -> Result<()> { | |
| // SAFETY: The params we pass here are valid for the duration of `CgptAdd`, as | |
| // made sure by the lifetime of `self`. | |
| let err = unsafe { vboot_host::CgptAdd(&mut self.params) }; | |
| if err != 0 { | |
| bail!("CgptAdd returned error status code {err}"); | |
| } | |
| Ok(()) | |
| } | |
| } | |
| /// Returns valid [`vboot_host::CgptAddParams`] for the given arguments. | |
| /// [`vboot_host::CgptAdd`] (which takes those arguments), is used for | |
| /// both adding partitions as well as modifying partitions. The caller | |
| /// controls the behaviour with supplying the correct arguments here. | |
| fn get_cgpt_add_params<'args, 'params>( | |
| index: u32, | |
| disk_path: &'args CStr, | |
| label: Option<&'args CStr>, | |
| begin: Option<u64>, | |
| size: Option<u64>, | |
| type_guid: Option<Guid>, | |
| ) -> CgptAddParamsWrapper<'params> | |
| where | |
| 'args: 'params, | |
| { | |
| let set_type_guid: i32 = if type_guid.is_some() { 1 } else { 0 }; | |
| let set_begin: i32 = if begin.is_some() { 1 } else { 0 }; | |
| let set_size: i32 = if size.is_some() { 1 } else { 0 }; | |
| let type_guid_c = type_guid.map_or(empty_guid(), to_cgpt_guid); | |
| let label_ptr = label.map_or(null(), |label_str| label_str.as_ptr()); | |
| CgptAddParamsWrapper { | |
| params: vboot_host::CgptAddParams { | |
| // Use the default value. | |
| drive_size: 0, | |
| unique_guid: empty_guid(), | |
| error_counter: 0, | |
| successful: 0, | |
| tries: 0, | |
| priority: 0, | |
| required: 0, | |
| legacy_boot: 0, | |
| raw_value: 0, | |
| // Passed by the caller. | |
| begin: begin.unwrap_or(0), | |
| partition: index, | |
| drive_name: disk_path.as_ptr(), | |
| size: size.unwrap_or(0), | |
| type_guid: type_guid_c, | |
| label: label_ptr, | |
| // For each of these fields, a value of 1 means to update | |
| // the value in the partition, and a value of 0 means don't | |
| // update it. | |
| set_begin, | |
| set_size, | |
| set_type: set_type_guid, | |
| set_unique: 0, | |
| set_error_counter: 0, | |
| set_successful: 0, | |
| set_tries: 0, | |
| set_priority: 0, | |
| set_required: 0, | |
| set_legacy_boot: 0, | |
| set_raw: 0, | |
| }, | |
| phantom_data: PhantomData, | |
| } | |
| } | |
| /// Creates a [`vboot_host::Guid`] from a [`gpt_disk_types::Guid`]. This | |
| /// conversion relies on the fact that it is only possible to obtain valid guids | |
| /// using [`gpt_disk_types::Guid`], so it just places the raw bytes of the guid. | |
| fn to_cgpt_guid(guid: Guid) -> vboot_host::Guid { | |
| vboot_host::Guid { | |
| u: vboot_host::Guid__bindgen_ty_1 { | |
| raw: guid.to_bytes(), | |
| }, | |
| } | |
| } | |
| /// Returns an empty [`vboot_host::Guid`]. | |
| fn empty_guid() -> vboot_host::Guid { | |
| vboot_host::Guid { | |
| u: vboot_host::Guid__bindgen_ty_1 { raw: [0; 16] }, | |
| } | |
| } | |
| /// This needs to be defined here since CgptAdd needs this present. In the cgpt | |
| /// binary this is replaced by a call to libuuid, but we don't link against that | |
| /// and need to provide this on our own. | |
| #[no_mangle] | |
| pub unsafe extern "C" fn GenerateGuid(newguid: *mut std::ffi::c_void) -> std::ffi::c_int { | |
| let uuid = Guid::from_random_bytes(random()).to_bytes(); | |
| let newguid: *mut vboot_host::Guid = newguid.cast(); | |
| (*newguid).u = vboot_host::Guid__bindgen_ty_1 { | |
| // Use a hardcoded GUID for now. | |
| raw: uuid, | |
| }; | |
| 0 | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use crate::gpt::Gpt; | |
| use super::*; | |
| use std::{fs::File, process::Command}; | |
| use anyhow::Result; | |
| use gpt_disk_types::{BlockSize, Lba}; | |
| const TEST_IMAGE_PATH: &str = "fake_disk.bin"; | |
| const NUM_SECTORS: u32 = 1000; | |
| const DATA_START: u64 = 100; | |
| const DATA_SIZE: u64 = 20; | |
| const DATA_LABEL: &str = "data stuff"; | |
| const DATA_GUID: &str = "0fc63daf-8483-4772-8e79-3d69d8477de4"; | |
| const DATA_NUM: u32 = 1; | |
| fn setup_test_disk_environment() -> Result<tempfile::TempDir> { | |
| // Create a tempdir to mount a tempfs to. | |
| let tmp_dir = tempfile::tempdir()?; | |
| let path = tmp_dir.path().join(TEST_IMAGE_PATH); | |
| let path = path.as_path(); | |
| // Create a fake disk at path. | |
| let status = Command::new("dd") | |
| .arg("if=/dev/zero") | |
| .arg(&format!("of={}", path.display())) | |
| .arg("conv=fsync") | |
| .arg("bs=512") | |
| .arg(&format!("count={NUM_SECTORS}")) | |
| .status()?; | |
| assert!(status.success()); | |
| // Create a GPT header table. | |
| let status = Command::new("cgpt").arg("create").arg(path).status()?; | |
| assert!(status.success()); | |
| // Add the initial data partition. | |
| let status = Command::new("cgpt") | |
| .arg("add") | |
| .args(["-b", &format!("{DATA_START}")]) | |
| .args(["-s", &format!("{DATA_SIZE}")]) | |
| .args(["-t", DATA_GUID]) | |
| .args(["-l", DATA_LABEL]) | |
| .arg(path) | |
| .status()?; | |
| assert!(status.success()); | |
| Ok(tmp_dir) | |
| } | |
| #[test] | |
| fn test_cgpt_resize_and_add() -> Result<()> { | |
| // Setup. | |
| let temp_dir = setup_test_disk_environment()?; | |
| let path = temp_dir.path().join(TEST_IMAGE_PATH); | |
| let path = path.as_path(); | |
| assert!(matches!(path.try_exists(), Ok(true))); | |
| // Test `resize_partition_gpt_partition`. | |
| let data_part_end = DATA_START + 2; | |
| let new_range = LbaRangeInclusive::new(Lba(DATA_START), Lba(data_part_end)).unwrap(); | |
| resize_cgpt_partition(DATA_NUM, path, DATA_LABEL, new_range)?; | |
| let file = File::open(path)?; | |
| let mut gpt = Gpt::from_file(file, BlockSize::BS_512)?; | |
| let partition = gpt.get_entry_for_partition_with_label(DATA_LABEL.parse()?); | |
| assert!(partition.is_ok()); | |
| assert_eq!(partition.unwrap().ending_lba.to_u64(), data_part_end); | |
| // Test `add_cgpt_partition`. | |
| let new_part_end = DATA_START + 4; | |
| let range = LbaRangeInclusive::new(Lba(data_part_end + 1), Lba(new_part_end)).unwrap(); | |
| let new_part_label = "TEST".to_string(); | |
| add_cgpt_partition(DATA_NUM + 1, path, &new_part_label, range)?; | |
| let file = File::open(path)?; | |
| let mut gpt = Gpt::from_file(file, BlockSize::BS_512)?; | |
| let partition = gpt.get_entry_for_partition_with_label(new_part_label.parse()?); | |
| assert!(partition.is_ok()); | |
| let partition = partition.unwrap(); | |
| assert_eq!(partition.starting_lba.to_u64(), data_part_end + 1); | |
| assert_eq!(partition.ending_lba.to_u64(), new_part_end); | |
| // Test `remove_cgpt_partition`. | |
| remove_cgpt_partition(DATA_NUM + 1, path)?; | |
| let file = File::open(path)?; | |
| let mut gpt = Gpt::from_file(file, BlockSize::BS_512)?; | |
| let partition = gpt.get_entry_for_partition_with_label(new_part_label.parse()?); | |
| assert!(partition.is_err()); | |
| Ok(()) | |
| } | |
| } |