| // Copyright 2024, The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| //! Fastboot backend for libgbl. |
| |
| use crate::{ |
| fuchsia_boot::GblAbrOps, |
| gbl_print, gbl_println, |
| partition::{check_part_unique, GblDisk, PartitionIo}, |
| GblOps, |
| }; |
| pub use abr::{mark_slot_active, set_one_shot_bootloader, set_one_shot_recovery, SlotIndex}; |
| use core::{ |
| array::from_fn, cmp::min, ffi::CStr, fmt::Write, future::Future, marker::PhantomData, |
| mem::take, ops::DerefMut, pin::Pin, str::from_utf8, |
| }; |
| use fastboot::{ |
| next_arg, next_arg_u64, process_next_command, run_tcp_session, CommandError, CommandResult, |
| FastbootImplementation, InfoSender, OkaySender, RebootMode, UploadBuilder, Uploader, |
| VarInfoSender, MAX_COMMAND_SIZE, |
| }; |
| use gbl_async::{join, yield_now}; |
| use gbl_storage::{BlockIo, Disk, Gpt}; |
| use liberror::Error; |
| use libutils::FormattedBytes; |
| use safemath::SafeNum; |
| use zbi::{ZbiContainer, ZbiType}; |
| |
| mod vars; |
| |
| pub(crate) mod sparse; |
| use sparse::is_sparse_image; |
| |
| mod shared; |
| pub use shared::Shared; |
| |
| mod buffer_pool; |
| pub use buffer_pool::BufferPool; |
| use buffer_pool::ScopedBuffer; |
| |
| mod pin_fut_container; |
| pub use pin_fut_container::PinFutContainer; |
| use pin_fut_container::{PinFutContainerTyped, PinFutSlice}; |
| |
| // Re-exports dependency types |
| pub use fastboot::{TcpStream, Transport}; |
| |
| /// Reserved name for indicating flashing GPT. |
| const FLASH_GPT_PART: &str = "gpt"; |
| |
| /// Represents the workload of a GBL Fastboot async task. |
| enum TaskWorkload<'a, 'b, B: BlockIo, P: BufferPool> { |
| /// Image flashing task. (partition io, downloaded data, data size) |
| Flash(PartitionIo<'a, B>, ScopedBuffer<'b, P>, usize), |
| // Image erase task. |
| Erase(PartitionIo<'a, B>, ScopedBuffer<'b, P>), |
| None, |
| } |
| |
| impl<'a, 'b, B: BlockIo, P: BufferPool> TaskWorkload<'a, 'b, B, P> { |
| /// Runs the task and returns the result |
| async fn run(self) -> Result<(), Error> { |
| match self { |
| Self::Flash(mut part_io, mut download, data_size) => match is_sparse_image(&download) { |
| Ok(_) => part_io.write_sparse(0, &mut download).await, |
| _ => part_io.write(0, &mut download[..data_size]).await, |
| }, |
| Self::Erase(mut part_io, mut buffer) => part_io.zeroize(&mut buffer).await, |
| _ => Ok(()), |
| } |
| } |
| } |
| |
| /// Represents a GBL Fastboot async task. |
| struct Task<'a, 'b, B: BlockIo, P: BufferPool> { |
| workload: TaskWorkload<'a, 'b, B, P>, |
| context: [u8; MAX_COMMAND_SIZE], |
| } |
| |
| impl<'a, 'b, B: BlockIo, P: BufferPool> Task<'a, 'b, B, P> { |
| /// Creates a new instance with the given workload. |
| fn new(workload: TaskWorkload<'a, 'b, B, P>) -> Self { |
| Self { workload, context: [0u8; MAX_COMMAND_SIZE] } |
| } |
| |
| /// Sets the context string. |
| fn set_context(&mut self, mut f: impl FnMut(&mut dyn Write) -> Result<(), core::fmt::Error>) { |
| let _ = f(&mut FormattedBytes::new(&mut self.context[..])); |
| } |
| |
| /// Runs the task and returns the result. |
| async fn run_checked(self) -> Result<(), Error> { |
| self.workload.run().await |
| } |
| |
| /// Runs the task. Panics on error. |
| /// |
| /// The method is intended for use in the context of parallel/background async task where errors |
| /// can't be easily handled by the main routine. |
| async fn run(self) { |
| match self.workload.run().await { |
| Err(e) => panic!( |
| "A Fastboot async task failed: {e:?}, context: {}", |
| from_utf8(&self.context[..]).unwrap_or("") |
| ), |
| _ => {} |
| } |
| } |
| } |
| |
| impl<'a, 'b, B: BlockIo, P: BufferPool> Default for Task<'a, 'b, B, P> { |
| fn default() -> Self { |
| // Creates a noop task. This is mainly used for type inference for inline declaration of |
| // pre-allocated task pool. |
| Self::new(TaskWorkload::None) |
| } |
| } |
| |
| /// `GblFastboot` implements fastboot commands in the GBL context. |
| /// |
| /// # Lifetimes |
| /// |
| /// * `'a`: [GblOps] and disks lifetime. |
| /// * `'b`: Lifetime for the buffer allocated by `P`. |
| /// * `'c`: Lifetime of the pinned [Future]s in task container `task`. |
| /// * `'d`: Lifetime of the `tasks` and `gbl_ops` objects borrowed. |
| /// * `'e`: Lifetime of the ImageBuffers returned by `get_image_buffer()`. |
| /// |
| /// # Generics |
| /// |
| /// * `G`: Type of `Self::gbl_ops` which implements [GblOps]. |
| /// * `B`: Type that implements [BlockIo] in the [Disk] parameter of [GblDisk] for `Self::disks`. |
| /// * `S`: Type of scratch buffer in the [Disk] parameter of [GblDisk] for `Self::disks`. |
| /// * `T`: Type of gpt buffer in the [Gpt] parameter of [GblDisk] for `Self::disks`. |
| /// * `P`: Type of `Self::buffer_pool` which implements [BufferPool]. |
| /// * `C`: Type of `Self::tasks` which implements [PinFutContainerTyped]. |
| /// * `F`: Type of [Future] stored by `Self::Tasks`. |
| struct GblFastboot<'a, 'b, 'c, 'd, 'e, G, B, S, T, P, C, F> |
| where |
| G: GblOps<'a, 'e>, |
| B: BlockIo, |
| S: DerefMut<Target = [u8]>, |
| T: DerefMut<Target = [u8]>, |
| P: BufferPool, |
| { |
| pub(crate) gbl_ops: &'d mut G, |
| // We store the partition devices returned by `gbl_ops.disks()` directly instead of getting it |
| // from `gbl_ops` later because we need to establish to the compiler that the hidden type of |
| // [BlockIo] in `GblDisk<Disk<impl BlockIO...>...>` returned by `gbl_ops.disks()` will be the |
| // same as the [BlockIo] type (denoted as B) in the function pointer |
| // `task_mapper`: fn(Task<'a, 'b, B, P>) -> F`. Otherwise, compiler won't allow `fn flash()` |
| // to call `task_mapper` with a `Task` constructed from `GblDisk<Disk<impl BlockIO...>...>`. |
| disks: &'a [GblDisk<Disk<B, S>, Gpt<T>>], |
| buffer_pool: &'b Shared<P>, |
| task_mapper: fn(Task<'a, 'b, B, P>) -> F, |
| tasks: &'d Shared<C>, |
| current_download_buffer: Option<ScopedBuffer<'b, P>>, |
| current_download_size: usize, |
| enable_async_task: bool, |
| default_block: Option<usize>, |
| bootimg_buf: &'b mut [u8], |
| // Introduces marker type so that we can enforce constraint 'd <= min('b, 'c). |
| // The constraint is expressed in the implementation block for the `FastbootImplementation` |
| // trait. |
| _tasks_context_lifetime: PhantomData<&'c P>, |
| _get_image_buffer_lifetime: PhantomData<&'e ()>, |
| } |
| |
| // See definition of [GblFastboot] for docs on lifetimes and generics parameters. |
| impl<'a: 'c, 'b: 'c, 'c, 'd, 'e, G, B, S, T, P, C, F> |
| GblFastboot<'a, 'b, 'c, 'd, 'e, G, B, S, T, P, C, F> |
| where |
| G: GblOps<'a, 'e>, |
| B: BlockIo, |
| S: DerefMut<Target = [u8]>, |
| T: DerefMut<Target = [u8]>, |
| P: BufferPool, |
| C: PinFutContainerTyped<'c, F>, |
| F: Future<Output = ()> + 'c, |
| { |
| /// Creates a new [GblFastboot]. |
| /// |
| /// # Args |
| /// |
| /// * `gbl_ops`: An implementation of `GblOps`. |
| /// * `disks`: The disk devices returned by `gbl_ops.disks()`. This is needed for expressing the |
| /// property that the hidden [BlockIo] type is the same as that in `task_mapper`. |
| /// * `task_mapper`: A function pointer that maps `Task<'a, 'b, G, B>` to the target [Future] |
| /// type `F` for input to `PinFutContainerTyped<F>::add_with()`. |
| /// * `tasks`: A shared instance of `PinFutContainerTyped<F>`. |
| /// * `buffer_pool`: A shared instance of `BufferPool`. |
| /// |
| /// The combination of `task_mapper` and `tasks` allows type `F`, which will be running the |
| /// async function `Task::run()`, to be defined at the callsite. This is necessary for the |
| /// usage of preallocated pinned futures (by `run_gbl_fastboot_stack()`) because the returned |
| /// type of a `async fn` is compiler-generated and can't be named. The only way to create a |
| /// preallocated slice of anonymous future is to keep the type generic and pass in the |
| /// anonymous future instance at the initialization callsite (aka defining use) and let compiler |
| /// infer and propagate it. |
| fn new( |
| gbl_ops: &'d mut G, |
| disks: &'a [GblDisk<Disk<B, S>, Gpt<T>>], |
| task_mapper: fn(Task<'a, 'b, B, P>) -> F, |
| tasks: &'d Shared<C>, |
| buffer_pool: &'b Shared<P>, |
| bootimg_buf: &'b mut [u8], |
| ) -> Self { |
| Self { |
| gbl_ops, |
| disks, |
| task_mapper, |
| tasks, |
| buffer_pool, |
| current_download_buffer: None, |
| current_download_size: 0, |
| enable_async_task: false, |
| default_block: None, |
| bootimg_buf, |
| _tasks_context_lifetime: PhantomData, |
| _get_image_buffer_lifetime: PhantomData, |
| } |
| } |
| |
| /// Returns the shared task container. |
| fn tasks(&self) -> &'d Shared<impl PinFutContainerTyped<'c, F>> { |
| self.tasks |
| } |
| |
| /// Listens on the given USB/TCP channels and runs fastboot. |
| async fn run( |
| &mut self, |
| mut usb: Option<impl GblUsbTransport>, |
| mut tcp: Option<impl GblTcpStream>, |
| ) { |
| if usb.is_none() && tcp.is_none() { |
| gbl_println!(self.gbl_ops, "No USB or TCP found for GBL Fastboot"); |
| return; |
| } |
| let tasks = self.tasks(); |
| // The fastboot command loop task for interacting with the remote host. |
| let cmd_loop_end = Shared::from(false); |
| let cmd_loop_task = async { |
| loop { |
| if let Some(v) = usb.as_mut() { |
| if v.has_packet() { |
| let res = match process_next_command(v, self).await { |
| Ok(true) => break, |
| v => v, |
| }; |
| if res.is_err() { |
| gbl_println!(self.gbl_ops, "GBL Fastboot USB session error: {:?}", res); |
| } |
| } |
| } |
| |
| if let Some(v) = tcp.as_mut() { |
| if v.accept_new() { |
| let res = match run_tcp_session(v, self).await { |
| Ok(()) => break, |
| v => v, |
| }; |
| if res.is_err_and(|e| e != Error::Disconnected) { |
| gbl_println!(self.gbl_ops, "GBL Fastboot TCP session error: {:?}", res); |
| } |
| } |
| } |
| |
| yield_now().await; |
| } |
| *cmd_loop_end.borrow_mut() = true; |
| }; |
| |
| // Schedules [Task] spawned by GBL fastboot. |
| let gbl_fb_tasks = async { |
| while tasks.borrow_mut().poll_all() > 0 || !*cmd_loop_end.borrow_mut() { |
| yield_now().await; |
| } |
| }; |
| |
| let _ = join(cmd_loop_task, gbl_fb_tasks).await; |
| } |
| |
| /// Extracts the next argument and verifies that it is a valid block device ID if present. |
| /// |
| /// # Returns |
| /// |
| /// * Returns `Ok(Some(blk_id))` if next argument is present and is a valid block device ID. |
| /// * Returns `None` if next argument is not available and there are more than one block |
| /// devices. |
| /// * Returns `Err(())` if next argument is present but is an invalid block device ID. |
| fn check_next_arg_blk_id<'s>( |
| &self, |
| args: &mut impl Iterator<Item = &'s str>, |
| ) -> CommandResult<Option<usize>> { |
| let devs = self.disks; |
| let blk_id = match next_arg_u64(args)? { |
| Some(v) => { |
| let v = usize::try_from(v)?; |
| // Checks out of range. |
| devs.get(v).ok_or("Invalid block ID")?; |
| Some(v) |
| } |
| _ => None, |
| }; |
| let blk_id = blk_id.or(self.default_block); |
| let blk_id = blk_id.or((devs.len() == 1).then_some(0)); |
| Ok(blk_id) |
| } |
| |
| /// Parses and checks the argument for "fastboot flash gpt/<blk_idx>/"resize". |
| /// |
| /// # Returns |
| /// |
| /// * Returns `Ok(Some((blk_idx, resize)))` if command is a GPT flashing command. |
| /// * Returns `Ok(None)` if command is not a GPT flashing command. |
| /// * Returns `Err()` otherwise. |
| pub(crate) fn parse_flash_gpt_args(&self, part: &str) -> CommandResult<Option<(usize, bool)>> { |
| // Syntax: flash gpt/<blk_idx>/"resize" |
| let mut args = part.split('/'); |
| if next_arg(&mut args).filter(|v| *v == FLASH_GPT_PART).is_none() { |
| return Ok(None); |
| } |
| // Parses block device ID. |
| let blk_id = self |
| .check_next_arg_blk_id(&mut args)? |
| .ok_or("Block ID is required for flashing GPT")?; |
| // Parses resize option. |
| let resize = match next_arg(&mut args) { |
| Some("resize") => true, |
| Some(_) => return Err("Unknown argument".into()), |
| _ => false, |
| }; |
| Ok(Some((blk_id, resize))) |
| } |
| |
| /// Parses and checks the partition argument and returns the partition name, block device |
| /// index, start offset and size. |
| pub(crate) fn parse_partition<'s>( |
| &self, |
| part: &'s str, |
| ) -> CommandResult<(Option<&'s str>, usize, u64, u64)> { |
| let devs = self.disks; |
| let mut args = part.split('/'); |
| // Parses partition name. |
| let part = next_arg(&mut args); |
| // Parses block device ID. |
| let blk_id = self.check_next_arg_blk_id(&mut args)?; |
| // Parses sub window offset. |
| let window_offset = next_arg_u64(&mut args)?.unwrap_or(0); |
| // Parses sub window size. |
| let window_size = next_arg_u64(&mut args)?; |
| // Checks uniqueness of the partition and resolves its block device ID. |
| let find = |p: Option<&'s str>| match blk_id { |
| None => Ok((check_part_unique(devs, p.ok_or("Must provide a partition")?)?, p)), |
| Some(v) => Ok(((v, devs[v].find_partition(p)?), p)), |
| }; |
| let ((blk_id, partition), actual) = match find(part) { |
| // Some legacy Fuchsia devices in the field uses name "fuchsia-fvm" for the standard |
| // "fvm" partition. However all of our infra uses the standard name "fvm" when flashing. |
| // Here we do a one off mapping if the device falls into this case. Once we have a |
| // solution for migrating those devices off the legacy name, we can remove this. |
| // |
| // If we run into more of such legacy aliases that we can't migrate, consider adding |
| // interfaces in GblOps for this. |
| Err(Error::NotFound) if part == Some("fvm") => find(Some("fuchsia-fvm"))?, |
| v => v?, |
| }; |
| let part_sz = SafeNum::from(partition.size()?); |
| let window_size = window_size.unwrap_or((part_sz - window_offset).try_into()?); |
| u64::try_from(part_sz - window_size - window_offset)?; |
| Ok((actual, blk_id, window_offset, window_size)) |
| } |
| |
| /// Takes the download data and resets download size. |
| fn take_download(&mut self) -> Option<(ScopedBuffer<'b, P>, usize)> { |
| Some((self.current_download_buffer.take()?, take(&mut self.current_download_size))) |
| } |
| |
| /// Waits until a Disk device is ready and get the [PartitionIo] for `part`. |
| pub async fn wait_partition_io( |
| &self, |
| blk: usize, |
| part: Option<&str>, |
| ) -> CommandResult<PartitionIo<'a, B>> { |
| loop { |
| match self.disks[blk].partition_io(part) { |
| Err(Error::NotReady) => yield_now().await, |
| v => return Ok(v?), |
| } |
| } |
| } |
| |
| /// An internal helper for parsing a partition and getting the partition IO |
| async fn parse_and_get_partition_io( |
| &self, |
| part: &str, |
| ) -> CommandResult<(usize, PartitionIo<'a, B>)> { |
| let (part, blk_idx, start, sz) = self.parse_partition(part)?; |
| Ok((blk_idx, self.wait_partition_io(blk_idx, part).await?.sub(start, sz)?)) |
| } |
| |
| /// Helper for scheduiling an async task. |
| /// |
| /// * If `Self::enable_async_task` is true, the method will add the task to the background task |
| /// list. Otherwise it simply runs the task. |
| async fn schedule_task( |
| &mut self, |
| task: Task<'a, 'b, B, P>, |
| responder: &mut impl InfoSender, |
| ) -> CommandResult<()> { |
| Ok(match self.enable_async_task { |
| true => { |
| let mut t = Some((self.task_mapper)(task)); |
| self.tasks.borrow_mut().add_with(|| t.take().unwrap()); |
| while t.is_some() { |
| yield_now().await; |
| self.tasks.borrow_mut().add_with(|| t.take().unwrap()); |
| } |
| self.tasks.borrow_mut().poll_all(); |
| let info = |
| "An async task is launched. To sync manually, run \"oem gbl-sync-tasks\"."; |
| responder.send_info(info).await? |
| } |
| _ => task.run_checked().await?, |
| }) |
| } |
| |
| /// Waits for all block devices to be ready. |
| async fn sync_all_blocks(&self) -> CommandResult<()> { |
| for (idx, _) in self.disks.iter().enumerate() { |
| let _ = self.wait_partition_io(idx, None).await; |
| } |
| Ok(()) |
| } |
| |
| /// Implementation for "fastboot oem gbl-sync-tasks". |
| async fn oem_sync_tasks<'s>( |
| &self, |
| mut _responder: impl InfoSender, |
| _res: &'s mut [u8], |
| ) -> CommandResult<&'s [u8]> { |
| self.sync_all_blocks().await?; |
| Ok(b"") |
| } |
| |
| /// Syncs all storage devices and reboots. |
| async fn sync_tasks_and_reboot( |
| &mut self, |
| mode: RebootMode, |
| mut resp: impl InfoSender + OkaySender, |
| ) -> CommandResult<()> { |
| resp.send_info("Syncing storage...").await?; |
| self.sync_all_blocks().await?; |
| match mode { |
| RebootMode::Normal => { |
| resp.send_info("Rebooting...").await?; |
| resp.send_okay("").await?; |
| self.gbl_ops.reboot(); |
| } |
| RebootMode::Bootloader => { |
| let f = self.gbl_ops.reboot_bootloader()?; |
| resp.send_info("Rebooting to bootloader...").await?; |
| resp.send_okay("").await?; |
| f() |
| } |
| RebootMode::Recovery => { |
| let f = self.gbl_ops.reboot_recovery()?; |
| resp.send_info("Rebooting to recovery...").await?; |
| resp.send_okay("").await?; |
| f() |
| } |
| _ => return Err("Unsupported".into()), |
| } |
| Ok(()) |
| } |
| |
| /// Appends a staged payload as bootloader file. |
| async fn add_staged_bootloader_file(&mut self, file_name: &str) -> CommandResult<()> { |
| let buffer = self |
| .gbl_ops |
| .get_zbi_bootloader_files_buffer_aligned() |
| .ok_or("No ZBI bootloader file buffer is provided")?; |
| let data = self.current_download_buffer.as_mut().ok_or("No file staged")?; |
| let data = &mut data[..self.current_download_size]; |
| let mut zbi = match ZbiContainer::parse(&mut buffer[..]) { |
| Ok(v) => v, |
| _ => ZbiContainer::new(&mut buffer[..])?, |
| }; |
| let next_payload = zbi.get_next_payload()?; |
| // Format: name length (1 byte) | name | file content. |
| let (name_len, rest) = next_payload.split_at_mut_checked(1).ok_or("Buffer too small")?; |
| let (name, rest) = rest.split_at_mut_checked(file_name.len()).ok_or("Buffer too small")?; |
| let file_content = rest.get_mut(..data.len()).ok_or("Buffer too small")?; |
| name_len[0] = file_name.len().try_into().map_err(|_| "File name length overflows 256")?; |
| name.clone_from_slice(file_name.as_bytes()); |
| file_content.clone_from_slice(data); |
| // Creates the entry; |
| zbi.create_entry( |
| ZbiType::BootloaderFile, |
| 0, |
| Default::default(), |
| 1 + file_name.len() + data.len(), |
| )?; |
| Ok(()) |
| } |
| } |
| |
| // See definition of [GblFastboot] for docs on lifetimes and generics parameters. |
| impl<'a: 'c, 'b: 'c, 'c, 'e, G, B, S, T, P, C, F> FastbootImplementation |
| for GblFastboot<'a, 'b, 'c, '_, 'e, G, B, S, T, P, C, F> |
| where |
| G: GblOps<'a, 'e>, |
| B: BlockIo, |
| S: DerefMut<Target = [u8]>, |
| T: DerefMut<Target = [u8]>, |
| P: BufferPool, |
| C: PinFutContainerTyped<'c, F>, |
| F: Future<Output = ()> + 'c, |
| { |
| async fn get_var( |
| &mut self, |
| var: &CStr, |
| args: impl Iterator<Item = &'_ CStr> + Clone, |
| out: &mut [u8], |
| _: impl InfoSender, |
| ) -> CommandResult<usize> { |
| Ok(self.get_var_internal(var, args, out)?.len()) |
| } |
| |
| async fn get_var_all(&mut self, mut resp: impl VarInfoSender) -> CommandResult<()> { |
| self.get_var_all_internal(&mut resp).await |
| } |
| |
| async fn get_download_buffer(&mut self) -> &mut [u8] { |
| if self.current_download_buffer.is_none() { |
| self.current_download_buffer = Some(self.buffer_pool.allocate_async().await); |
| } |
| self.current_download_buffer.as_mut().unwrap() |
| } |
| |
| async fn download_complete( |
| &mut self, |
| download_size: usize, |
| _: impl InfoSender, |
| ) -> CommandResult<()> { |
| self.current_download_size = download_size; |
| Ok(()) |
| } |
| |
| async fn flash(&mut self, part: &str, mut responder: impl InfoSender) -> CommandResult<()> { |
| let disks = self.disks; |
| |
| // Checks if we are flashing new GPT partition table |
| if let Some((blk_idx, resize)) = self.parse_flash_gpt_args(part)? { |
| self.wait_partition_io(blk_idx, None).await?; |
| let (mut gpt, size) = self.take_download().ok_or("No GPT downloaded")?; |
| responder.send_info("Updating GPT...").await?; |
| return match disks[blk_idx].update_gpt(&mut gpt[..size], resize).await { |
| Err(Error::NotReady) => panic!("Should not be busy"), |
| Err(Error::Unsupported) => Err("Block device is not for GPT".into()), |
| v => Ok(v?), |
| }; |
| } |
| |
| let (_, part_io) = self.parse_and_get_partition_io(part).await?; |
| let (download_buffer, data_size) = self.take_download().ok_or("No download")?; |
| let mut task = Task::new(TaskWorkload::Flash(part_io, download_buffer, data_size)); |
| task.set_context(|f| write!(f, "flash:{part}")); |
| Ok(self.schedule_task(task, &mut responder).await?) |
| } |
| |
| async fn erase(&mut self, part: &str, mut responder: impl InfoSender) -> CommandResult<()> { |
| let disks = self.disks; |
| |
| // Checks if we are erasing GPT partition table. |
| if let Some((blk_idx, _)) = self.parse_flash_gpt_args(part)? { |
| self.wait_partition_io(blk_idx, None).await?; |
| return match disks[blk_idx].erase_gpt().await { |
| Err(Error::NotReady) => panic!("Should not be busy"), |
| Err(Error::Unsupported) => Err("Block device is not for GPT".into()), |
| v => Ok(v?), |
| }; |
| } |
| |
| let (_, part_io) = self.parse_and_get_partition_io(part).await?; |
| self.get_download_buffer().await; |
| let mut task = Task::new(TaskWorkload::Erase(part_io, self.take_download().unwrap().0)); |
| task.set_context(|f| write!(f, "erase:{part}")); |
| Ok(self.schedule_task(task, &mut responder).await?) |
| } |
| |
| async fn upload(&mut self, _: impl UploadBuilder) -> CommandResult<()> { |
| Err("Unimplemented".into()) |
| } |
| |
| async fn fetch( |
| &mut self, |
| part: &str, |
| offset: u64, |
| size: u64, |
| mut responder: impl UploadBuilder + InfoSender, |
| ) -> CommandResult<()> { |
| let (_, mut part_io) = self.parse_and_get_partition_io(part).await?; |
| let buffer = self.get_download_buffer().await; |
| let end = u64::try_from(SafeNum::from(offset) + size)?; |
| let mut curr = offset; |
| responder |
| .send_formatted_info(|v| write!(v, "Uploading {} bytes...", size).unwrap()) |
| .await?; |
| let mut uploader = responder.initiate_upload(size).await?; |
| while curr < end { |
| let to_send = min(usize::try_from(end - curr)?, buffer.len()); |
| part_io.read(curr, &mut buffer[..to_send]).await?; |
| uploader.upload(&mut buffer[..to_send]).await?; |
| curr += u64::try_from(to_send)?; |
| } |
| Ok(()) |
| } |
| |
| async fn reboot( |
| &mut self, |
| mode: RebootMode, |
| resp: impl InfoSender + OkaySender, |
| ) -> CommandError { |
| match self.sync_tasks_and_reboot(mode, resp).await { |
| Err(e) => e, |
| _ => "Unknown".into(), |
| } |
| } |
| |
| async fn r#continue(&mut self, mut resp: impl InfoSender) -> CommandResult<()> { |
| resp.send_info("Syncing storage...").await?; |
| Ok(self.sync_all_blocks().await?) |
| } |
| |
| async fn set_active(&mut self, slot: &str, _: impl InfoSender) -> CommandResult<()> { |
| self.sync_all_blocks().await?; |
| match self.gbl_ops.expected_os_is_fuchsia()? { |
| // TODO(b/374776896): Prioritizes platform specific `set_active_slot` if available. |
| true => Ok(mark_slot_active( |
| &mut GblAbrOps(self.gbl_ops), |
| match slot { |
| "a" => SlotIndex::A, |
| "b" => SlotIndex::B, |
| _ => return Err("Invalid slot index for Fuchsia A/B/R".into()), |
| }, |
| )?), |
| _ => Err("Not supported".into()), |
| } |
| } |
| |
| async fn oem<'s>( |
| &mut self, |
| cmd: &str, |
| mut responder: impl InfoSender, |
| res: &'s mut [u8], |
| ) -> CommandResult<&'s [u8]> { |
| let mut args = cmd.split(' '); |
| let cmd = args.next().ok_or("Missing command")?; |
| match cmd { |
| "gbl-sync-tasks" => self.oem_sync_tasks(responder, res).await, |
| "gbl-enable-async-task" => { |
| self.enable_async_task = true; |
| Ok(b"") |
| } |
| "gbl-disable-async-task" => { |
| self.enable_async_task = false; |
| Ok(b"") |
| } |
| "gbl-unset-default-block" => { |
| self.default_block = None; |
| Ok(b"") |
| } |
| "gbl-set-default-block" => { |
| let id = next_arg_u64(&mut args)?.ok_or("Missing block device ID")?; |
| let id = usize::try_from(id)?; |
| self.disks.get(id).ok_or("Out of range")?; |
| self.default_block = Some(id.try_into()?); |
| responder |
| .send_formatted_info(|f| write!(f, "Default block device: {id:#x}").unwrap()) |
| .await?; |
| Ok(b"") |
| } |
| "add-staged-bootloader-file" => { |
| let file_name = next_arg(&mut args).ok_or("Missing file name")?; |
| self.add_staged_bootloader_file(file_name).await?; |
| Ok(b"") |
| } |
| _ => Err("Unknown oem command".into()), |
| } |
| } |
| |
| async fn boot(&mut self, mut resp: impl InfoSender + OkaySender) -> CommandResult<()> { |
| let len = core::cmp::min(self.bootimg_buf.len(), self.current_download_size); |
| let data = self.current_download_buffer.as_mut().ok_or("No file staged")?; |
| let data = &mut data[..self.current_download_size]; |
| |
| self.bootimg_buf[..len].copy_from_slice(&data[..len]); |
| resp.send_info("Boot into boot.img").await?; |
| Ok(()) |
| } |
| } |
| |
| /// `GblUsbTransport` defines transport interfaces for running GBL fastboot over USB. |
| pub trait GblUsbTransport: Transport { |
| /// Checks whether there is a new USB packet. |
| fn has_packet(&mut self) -> bool; |
| } |
| |
| /// `GblTcpStream` defines transport interfaces for running GBL fastboot over TCP. |
| pub trait GblTcpStream: TcpStream { |
| /// Accepts a new TCP connection. |
| /// |
| /// If a connection is in progress, it should be aborted first. |
| /// |
| /// Returns true if a new connection is established, false otherwise. |
| fn accept_new(&mut self) -> bool; |
| } |
| |
| /// Runs GBL fastboot on the given USB/TCP channels. |
| /// |
| /// # Args: |
| /// |
| /// * `gbl_ops`: An instance of [GblOps]. |
| /// * `buffer_pool`: An implementation of [BufferPool]. |
| /// * `tasks`: An implementation of [PinFutContainer] |
| /// * `usb`: An optional implementation of [GblUsbTransport]. |
| /// * `tcp`: An optional implementation of [GblTcpStream]. |
| /// |
| /// # Lifetimes |
| /// * `'a`: Lifetime of [GblOps]. |
| /// * `'b`: Lifetime of `download_buffers`. |
| /// * `'c`: Lifetime of `tasks`. |
| pub async fn run_gbl_fastboot<'a: 'c, 'b: 'c, 'c, 'd>( |
| gbl_ops: &mut impl GblOps<'a, 'd>, |
| buffer_pool: &'b Shared<impl BufferPool>, |
| tasks: impl PinFutContainer<'c> + 'c, |
| usb: Option<impl GblUsbTransport>, |
| tcp: Option<impl GblTcpStream>, |
| bootimg_buf: &'b mut [u8], |
| ) { |
| let tasks = tasks.into(); |
| let disks = gbl_ops.disks(); |
| GblFastboot::new(gbl_ops, disks, Task::run, &tasks, buffer_pool, bootimg_buf) |
| .run(usb, tcp) |
| .await; |
| } |
| |
| /// Runs GBL fastboot on the given USB/TCP channels with N stack allocated worker tasks. |
| /// |
| /// The choice of N depends on the level of parallelism the platform can support. For platform with |
| /// `n` storage devices that can independently perform non-blocking IO, it will required `N = n` |
| /// and a `buffer_pool` that can allocate at least n+1 buffers at the same time in order to achieve |
| /// parallel flashing to all storages plus a parallel downloading. However, it is common for |
| /// disks that need to be flashed to be on the same block deviece so flashing of them becomes |
| /// sequential, in which case N can be smaller. Caller should take into consideration usage pattern |
| /// for determining N. |
| /// |
| /// # Args: |
| /// |
| /// * `gbl_ops`: An instance of [GblOps]. |
| /// * `buffer_pool`: An implementation of [BufferPool]. |
| /// * `usb`: An optional implementation of [GblUsbTransport]. |
| /// * `tcp`: An optional implementation of [GblTcpStream]. |
| pub async fn run_gbl_fastboot_stack<'a, 'b, const N: usize>( |
| gbl_ops: &mut impl GblOps<'a, 'b>, |
| buffer_pool: impl BufferPool, |
| usb: Option<impl GblUsbTransport>, |
| tcp: Option<impl GblTcpStream>, |
| bootimg_buf: &mut [u8], |
| ) { |
| let buffer_pool = buffer_pool.into(); |
| // Creates N worker tasks. |
| let mut tasks: [_; N] = from_fn(|_| Task::default().run()); |
| // It is possible to avoid the use of the unsafe `Pin::new_unchecked` by delaring the array and |
| // manually pinning each element i.e. |
| // |
| // ``` |
| // let mut tasks = [ |
| // core::pin::pin!(Task::None.run()), |
| // core::pin::pin!(Task::None.run()), |
| // core::pin::pin!(Task::None.run()), |
| // ]; |
| // ``` |
| // |
| // Parameterization of `N` will be an issue, but might be solvable with procedural macro. |
| // SAFETY: `tasks` is immediately shadowed and thus guaranteed not moved for the rest of its |
| // lifetime. |
| let mut tasks: [_; N] = tasks.each_mut().map(|v| unsafe { Pin::new_unchecked(v) }); |
| let tasks = PinFutSlice::new(&mut tasks[..]).into(); |
| let disks = gbl_ops.disks(); |
| GblFastboot::new(gbl_ops, disks, Task::run, &tasks, &buffer_pool, bootimg_buf) |
| .run(usb, tcp) |
| .await; |
| } |
| |
| /// Pre-generates a Fuchsia Fastboot MDNS service broadcast packet. |
| /// |
| /// Fuchsia ffx development flow can detect fastboot devices that broadcast a "_fastboot·_tcp·local" |
| /// MDNS service. This API generates the broadcast MDNS packet for Ipv6. Caller is reponsible for |
| /// sending this packet via UDP at the following address and port (defined by MDNS): |
| /// |
| /// * ipv6: ff02::fb |
| /// * port: 5353 |
| /// |
| /// # Args |
| /// |
| /// * `node_name`: The Fuchsia node name for the service. Must be a 22 character ASCII string in the |
| /// format "fuchsia-xxxx-xxxx-xxxx". |
| /// * `ipv6_addr`: The Ipv6 address bytes. |
| /// |
| /// The packet generated by the API contains the given IPv6 address and a fuchsia node name derived |
| /// from the given ethernet mac address `eth_mac`. |
| pub fn fuchsia_fastboot_mdns_packet(node_name: &str, ipv6_addr: &[u8]) -> Result<[u8; 140], Error> { |
| // Pre-generated Fuchsia fastboot MDNS service packet template. |
| // It contains the node name and ipv6 address. We simply replace with the device's node name and |
| // ipv6 address. |
| let mut packet: [u8; 140] = [ |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x09, 0x5f, 0x66, |
| 0x61, 0x73, 0x74, 0x62, 0x6f, 0x6f, 0x74, 0x04, 0x5f, 0x74, 0x63, 0x70, 0x05, 0x6c, 0x6f, |
| 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x19, 0x16, |
| 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2d, 0x34, 0x38, 0x32, 0x31, 0x2d, 0x30, 0x62, |
| 0x33, 0x31, 0x2d, 0x65, 0x61, 0x66, 0x38, 0xc0, 0x0c, 0xc0, 0x2c, 0x00, 0x21, 0x80, 0x01, |
| 0x00, 0x00, 0x00, 0x78, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x15, 0xb2, 0x16, 0x66, 0x75, |
| 0x63, 0x68, 0x73, 0x69, 0x61, 0x2d, 0x34, 0x38, 0x32, 0x31, 0x2d, 0x30, 0x62, 0x33, 0x31, |
| 0x2d, 0x65, 0x61, 0x66, 0x38, 0xc0, 0x1b, 0xc0, 0x57, 0x00, 0x1c, 0x80, 0x01, 0x00, 0x00, |
| 0x00, 0x78, 0x00, 0x10, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4a, 0x21, 0x0b, |
| 0xff, 0xfe, 0x31, 0xea, 0xf8, |
| ]; |
| // Offsets to the fuchsia node name field. |
| const NODE_NAME_OFFSETS: &[usize; 2] = &[45, 88]; |
| // Offset to the IPv6 address field. |
| const IP6_ADDR_OFFSET: usize = 124; |
| |
| if node_name.as_bytes().len() != 22 { |
| return Err(Error::InvalidInput); |
| } |
| |
| for off in NODE_NAME_OFFSETS { |
| packet[*off..][..node_name.len()].clone_from_slice(node_name.as_bytes()); |
| } |
| packet[IP6_ADDR_OFFSET..][..ipv6_addr.len()].clone_from_slice(ipv6_addr); |
| Ok(packet) |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| use crate::{ |
| ops::test::{FakeGblOps, FakeGblOpsStorage}, |
| Os, |
| }; |
| use abr::{ |
| get_and_clear_one_shot_bootloader, get_boot_slot, mark_slot_unbootable, ABR_DATA_SIZE, |
| }; |
| use core::{ |
| mem::size_of, |
| pin::{pin, Pin}, |
| str::from_utf8, |
| }; |
| use fastboot::{test_utils::TestUploadBuilder, MAX_RESPONSE_SIZE}; |
| use gbl_async::{block_on, poll, poll_n_times}; |
| use gbl_storage::GPT_GUID_LEN; |
| use liberror::Error; |
| use spin::{Mutex, MutexGuard}; |
| use std::ffi::CString; |
| use std::{collections::VecDeque, io::Read}; |
| use zerocopy::AsBytes; |
| |
| /// A test implementation of [InfoSender] and [OkaySender]. |
| #[derive(Default)] |
| struct TestResponder { |
| okay_sent: Mutex<bool>, |
| info_messages: Mutex<Vec<String>>, |
| } |
| |
| impl InfoSender for &TestResponder { |
| async fn send_formatted_info<F: FnOnce(&mut dyn Write)>( |
| &mut self, |
| cb: F, |
| ) -> Result<(), Error> { |
| let mut msg: String = "".into(); |
| cb(&mut msg); |
| self.info_messages.try_lock().unwrap().push(msg); |
| Ok(()) |
| } |
| } |
| |
| impl OkaySender for &TestResponder { |
| /// Sends a Fastboot "INFO<`msg`>" packet. |
| async fn send_formatted_okay<F: FnOnce(&mut dyn Write)>(self, _: F) -> Result<(), Error> { |
| *self.okay_sent.try_lock().unwrap() = true; |
| Ok(()) |
| } |
| } |
| |
| /// Helper to test fastboot variable value. |
| fn check_var(gbl_fb: &mut impl FastbootImplementation, var: &str, args: &str, expected: &str) { |
| let resp: TestResponder = Default::default(); |
| let args_c = args.split(':').map(|v| CString::new(v).unwrap()).collect::<Vec<_>>(); |
| let args_c = args_c.iter().map(|v| v.as_c_str()); |
| let var_c = CString::new(var).unwrap(); |
| let mut out = vec![0u8; MAX_RESPONSE_SIZE]; |
| let val = |
| block_on(gbl_fb.get_var_as_str(var_c.as_c_str(), args_c, &resp, &mut out[..])).unwrap(); |
| assert_eq!(val, expected, "var {}:{} = {} != {}", var, args, val, expected,); |
| } |
| |
| /// A helper to set the download content. |
| fn set_download(gbl_fb: &mut impl FastbootImplementation, data: &[u8]) { |
| block_on(gbl_fb.get_download_buffer())[..data.len()].clone_from_slice(data); |
| block_on(gbl_fb.download_complete(data.len(), &TestResponder::default())).unwrap(); |
| } |
| |
| impl<'a> PinFutContainer<'a> for Vec<Pin<Box<dyn Future<Output = ()> + 'a>>> { |
| fn add_with<F: Future<Output = ()> + 'a>(&mut self, f: impl FnOnce() -> F) { |
| self.push(Box::pin(f())); |
| } |
| |
| fn for_each_remove_if( |
| &mut self, |
| mut cb: impl FnMut(&mut Pin<&mut (dyn Future<Output = ()> + 'a)>) -> bool, |
| ) { |
| for idx in (0..self.len()).rev() { |
| cb(&mut self[idx].as_mut()).then(|| self.swap_remove(idx)); |
| } |
| } |
| } |
| |
| #[test] |
| fn test_get_var_gbl() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let storage = FakeGblOpsStorage::default(); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| check_var( |
| &mut gbl_fb, |
| FakeGblOps::GBL_TEST_VAR, |
| "arg", |
| format!("{}:Some(\"arg\")", FakeGblOps::GBL_TEST_VAR_VAL).as_str(), |
| ); |
| } |
| |
| #[test] |
| fn test_get_var_partition_info() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| storage.add_raw_device(c"raw_0", [0xaau8; 4 * 1024]); |
| storage.add_raw_device(c"raw_1", [0x55u8; 8 * 1024]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| // Check different semantics |
| check_var(&mut gbl_fb, "partition-size", "boot_a", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a/", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a//", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a///", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a/0", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a/0/", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a//0", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a/0/0", "0x2000"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a//0x1000", "0x1000"); |
| |
| check_var(&mut gbl_fb, "partition-size", "boot_b/0", "0x3000"); |
| check_var(&mut gbl_fb, "partition-size", "vendor_boot_a/1", "0x1000"); |
| check_var(&mut gbl_fb, "partition-size", "vendor_boot_b/1", "0x1800"); |
| check_var(&mut gbl_fb, "partition-size", "boot_a//0x1000", "0x1000"); |
| check_var(&mut gbl_fb, "partition-size", "raw_0", "0x1000"); |
| check_var(&mut gbl_fb, "partition-size", "raw_1", "0x2000"); |
| |
| let resp: TestResponder = Default::default(); |
| let mut out = vec![0u8; MAX_RESPONSE_SIZE]; |
| assert!(block_on(gbl_fb.get_var_as_str( |
| c"partition", |
| [c"non-existent"].into_iter(), |
| &resp, |
| &mut out[..], |
| )) |
| .is_err()); |
| } |
| |
| /// `TestVarSender` implements `TestVarSender`. It stores outputs in a vector of string. |
| struct TestVarSender(Vec<String>); |
| |
| impl VarInfoSender for &mut TestVarSender { |
| async fn send_var_info( |
| &mut self, |
| name: &str, |
| args: impl IntoIterator<Item = &'_ str>, |
| val: &str, |
| ) -> Result<(), Error> { |
| let args = args.into_iter().collect::<Vec<_>>(); |
| self.0.push(format!("{}:{}: {}", name, args.join(":"), val)); |
| Ok(()) |
| } |
| } |
| |
| #[test] |
| fn test_get_var_all() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| storage.add_raw_device(c"raw_0", [0xaau8; 4 * 1024]); |
| storage.add_raw_device(c"raw_1", [0x55u8; 8 * 1024]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| let mut logger = TestVarSender(vec![]); |
| block_on(gbl_fb.get_var_all(&mut logger)).unwrap(); |
| assert_eq!( |
| logger.0, |
| [ |
| "version-bootloader:: 1.0", |
| "max-fetch-size:: 0xffffffffffffffff", |
| "block-device:0:total-blocks: 0x80", |
| "block-device:0:block-size: 0x200", |
| "block-device:0:status: idle", |
| "block-device:1:total-blocks: 0x100", |
| "block-device:1:block-size: 0x200", |
| "block-device:1:status: idle", |
| "block-device:2:total-blocks: 0x1000", |
| "block-device:2:block-size: 0x1", |
| "block-device:2:status: idle", |
| "block-device:3:total-blocks: 0x2000", |
| "block-device:3:block-size: 0x1", |
| "block-device:3:status: idle", |
| "gbl-default-block:: None", |
| "partition-size:boot_a/0: 0x2000", |
| "partition-type:boot_a/0: raw", |
| "partition-size:boot_b/0: 0x3000", |
| "partition-type:boot_b/0: raw", |
| "partition-size:vendor_boot_a/1: 0x1000", |
| "partition-type:vendor_boot_a/1: raw", |
| "partition-size:vendor_boot_b/1: 0x1800", |
| "partition-type:vendor_boot_b/1: raw", |
| "partition-size:raw_0/2: 0x1000", |
| "partition-type:raw_0/2: raw", |
| "partition-size:raw_1/3: 0x2000", |
| "partition-type:raw_1/3: raw", |
| format!("{}:1: {}:1", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_str(), |
| format!("{}:2: {}:2", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_str(), |
| ] |
| ); |
| } |
| |
| /// A helper for fetching partition from a `GblFastboot` |
| fn fetch<EOff: core::fmt::Debug, ESz: core::fmt::Debug>( |
| fb: &mut impl FastbootImplementation, |
| part: String, |
| off: impl TryInto<u64, Error = EOff>, |
| size: impl TryInto<u64, Error = ESz>, |
| ) -> CommandResult<Vec<u8>> { |
| let off = off.try_into().unwrap(); |
| let size = size.try_into().unwrap(); |
| let mut upload_out = vec![0u8; usize::try_from(size).unwrap()]; |
| let test_uploader = TestUploadBuilder(&mut upload_out[..]); |
| block_on(fb.fetch(part.as_str(), off, size, test_uploader))?; |
| Ok(upload_out) |
| } |
| |
| #[test] |
| fn test_fetch_invalid_partition_arg() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| // Missing mandatory block device ID for raw block partition. |
| assert!(fetch(&mut gbl_fb, "//0/0".into(), 0, 0).is_err()); |
| |
| // GPT partition does not exist. |
| assert!(fetch(&mut gbl_fb, "non///".into(), 0, 0).is_err()); |
| |
| // GPT Partition is not unique. |
| assert!(fetch(&mut gbl_fb, "vendor_boot_a///".into(), 0, 0).is_err()); |
| |
| // Offset overflows. |
| assert!(fetch(&mut gbl_fb, "boot_a//0x2001/".into(), 0, 1).is_err()); |
| assert!(fetch(&mut gbl_fb, "boot_a".into(), 0x2000, 1).is_err()); |
| |
| // Size overflows. |
| assert!(fetch(&mut gbl_fb, "boot_a///0x2001".into(), 0, 0).is_err()); |
| assert!(fetch(&mut gbl_fb, "boot_a".into(), 0, 0x2001).is_err()); |
| } |
| |
| /// A helper for testing raw block upload. It verifies that data read from block device |
| /// `blk_id` in range [`off`, `off`+`size`) is the same as `disk[off..][..size]` |
| fn check_blk_upload( |
| fb: &mut impl FastbootImplementation, |
| blk_id: u64, |
| off: u64, |
| size: u64, |
| disk: &[u8], |
| ) { |
| let expected = disk[off.try_into().unwrap()..][..size.try_into().unwrap()].to_vec(); |
| // offset/size as part of the partition string. |
| let part = format!("/{:#x}/{:#x}/{:#x}", blk_id, off, size); |
| assert_eq!(fetch(fb, part, 0, size).unwrap(), expected); |
| // offset/size as separate fetch arguments. |
| let part = format!("/{:#x}", blk_id); |
| assert_eq!(fetch(fb, part, off, size).unwrap(), expected); |
| } |
| |
| #[test] |
| fn test_fetch_raw_block() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| let disk_0 = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let disk_1 = include_bytes!("../../../libstorage/test/gpt_test_2.bin"); |
| storage.add_gpt_device(disk_0); |
| storage.add_gpt_device(disk_1); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| let off = 512; |
| let size = 512; |
| check_blk_upload(&mut gbl_fb, 0, off, size, disk_0); |
| check_blk_upload(&mut gbl_fb, 1, off, size, disk_1); |
| } |
| |
| /// A helper for testing uploading GPT partition. It verifies that data read from GPT partition |
| /// `part` at disk `blk_id` in range [`off`, `off`+`size`) is the same as |
| /// `partition_data[off..][..size]`. |
| fn check_part_upload( |
| fb: &mut impl FastbootImplementation, |
| part: &str, |
| off: u64, |
| size: u64, |
| blk_id: Option<u64>, |
| partition_data: &[u8], |
| ) { |
| let expected = |
| partition_data[off.try_into().unwrap()..][..size.try_into().unwrap()].to_vec(); |
| let blk_id = blk_id.map_or("".to_string(), |v| format!("{:#x}", v)); |
| // offset/size as part of the partition string. |
| let gpt_part = format!("{}/{}/{:#x}/{:#x}", part, blk_id, off, size); |
| assert_eq!(fetch(fb, gpt_part, 0, size).unwrap(), expected); |
| // offset/size as separate fetch arguments. |
| let gpt_part = format!("{}/{}", part, blk_id); |
| assert_eq!(fetch(fb, gpt_part, off, size).unwrap(), expected); |
| } |
| |
| #[test] |
| fn test_fetch_partition() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| storage.add_raw_device(c"raw_0", [0xaau8; 4 * 1024]); |
| storage.add_raw_device(c"raw_1", [0x55u8; 8 * 1024]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| let expect_boot_a = include_bytes!("../../../libstorage/test/boot_a.bin"); |
| let expect_boot_b = include_bytes!("../../../libstorage/test/boot_b.bin"); |
| let expect_vendor_boot_a = include_bytes!("../../../libstorage/test/vendor_boot_a.bin"); |
| let expect_vendor_boot_b = include_bytes!("../../../libstorage/test/vendor_boot_b.bin"); |
| |
| let size = 512; |
| let off = 512; |
| |
| check_part_upload(&mut gbl_fb, "boot_a", off, size, Some(0), expect_boot_a); |
| check_part_upload(&mut gbl_fb, "boot_b", off, size, Some(0), expect_boot_b); |
| check_part_upload(&mut gbl_fb, "vendor_boot_a", off, size, Some(1), expect_vendor_boot_a); |
| check_part_upload(&mut gbl_fb, "vendor_boot_b", off, size, Some(1), expect_vendor_boot_b); |
| check_part_upload(&mut gbl_fb, "raw_0", off, size, Some(2), &[0xaau8; 4 * 1024]); |
| check_part_upload(&mut gbl_fb, "raw_1", off, size, Some(3), &[0x55u8; 8 * 1024]); |
| |
| // No block device id |
| check_part_upload(&mut gbl_fb, "boot_a", off, size, None, expect_boot_a); |
| check_part_upload(&mut gbl_fb, "boot_b", off, size, None, expect_boot_b); |
| check_part_upload(&mut gbl_fb, "vendor_boot_a", off, size, None, expect_vendor_boot_a); |
| check_part_upload(&mut gbl_fb, "vendor_boot_b", off, size, None, expect_vendor_boot_b); |
| check_part_upload(&mut gbl_fb, "raw_0", off, size, None, &[0xaau8; 4 * 1024]); |
| check_part_upload(&mut gbl_fb, "raw_1", off, size, None, &[0x55u8; 8 * 1024]); |
| } |
| |
| /// A helper function to get a bit-flipped copy of the input data. |
| fn flipped_bits(data: &[u8]) -> Vec<u8> { |
| data.iter().map(|v| !(*v)).collect::<Vec<_>>() |
| } |
| |
| /// A helper function to flash data to a partition |
| fn flash_part(fb: &mut impl FastbootImplementation, part: &str, data: &[u8]) { |
| // Prepare a download buffer. |
| let dl_size = data.len(); |
| let download = data.to_vec(); |
| let resp: TestResponder = Default::default(); |
| set_download(fb, &download[..]); |
| block_on(fb.flash(part, &resp)).unwrap(); |
| assert_eq!(fetch(fb, part.into(), 0, dl_size).unwrap(), download); |
| } |
| |
| /// A helper for testing partition flashing. |
| fn check_flash_part(fb: &mut impl FastbootImplementation, part: &str, expected: &[u8]) { |
| flash_part(fb, part, expected); |
| // Also flashes bit-wise reversed version in case the initial content is the same. |
| flash_part(fb, part, &flipped_bits(expected)); |
| } |
| |
| #[test] |
| fn test_flash_partition() { |
| let disk_0 = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let disk_1 = include_bytes!("../../../libstorage/test/gpt_test_2.bin"); |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(disk_0); |
| storage.add_gpt_device(disk_1); |
| storage.add_raw_device(c"raw_0", [0xaau8; 4 * 1024]); |
| storage.add_raw_device(c"raw_1", [0x55u8; 8 * 1024]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| let expect_boot_a = include_bytes!("../../../libstorage/test/boot_a.bin"); |
| let expect_boot_b = include_bytes!("../../../libstorage/test/boot_b.bin"); |
| check_flash_part(&mut gbl_fb, "boot_a", expect_boot_a); |
| check_flash_part(&mut gbl_fb, "boot_b", expect_boot_b); |
| check_flash_part(&mut gbl_fb, "raw_0", &[0xaau8; 4 * 1024]); |
| check_flash_part(&mut gbl_fb, "raw_1", &[0x55u8; 8 * 1024]); |
| check_flash_part(&mut gbl_fb, "/0", disk_0); |
| check_flash_part(&mut gbl_fb, "/1", disk_1); |
| |
| // Partital flash |
| let off = 0x200; |
| let size = 1024; |
| check_flash_part(&mut gbl_fb, "boot_a//200", &expect_boot_a[off..size]); |
| check_flash_part(&mut gbl_fb, "boot_b//200", &expect_boot_b[off..size]); |
| check_flash_part(&mut gbl_fb, "/0/200", &disk_0[off..size]); |
| check_flash_part(&mut gbl_fb, "/1/200", &disk_1[off..size]); |
| } |
| |
| #[test] |
| fn test_flash_partition_sparse() { |
| let raw = include_bytes!("../../testdata/sparse_test_raw.bin"); |
| let sparse = include_bytes!("../../testdata/sparse_test.bin"); |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"raw", vec![0u8; raw.len()]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| |
| let download = sparse.to_vec(); |
| let resp: TestResponder = Default::default(); |
| set_download(&mut gbl_fb, &download[..]); |
| block_on(gbl_fb.flash("/0", &resp)).unwrap(); |
| assert_eq!(fetch(&mut gbl_fb, "/0".into(), 0, raw.len()).unwrap(), raw); |
| } |
| |
| /// A helper to invoke OEM commands. |
| /// |
| /// Returns the result and INFO strings. |
| async fn oem( |
| fb: &mut impl FastbootImplementation, |
| oem_cmd: &str, |
| resp: impl InfoSender, |
| ) -> CommandResult<String> { |
| let mut res = [0u8; MAX_RESPONSE_SIZE]; |
| fb.oem(oem_cmd, resp, &mut res[..]).await?; |
| Ok(from_utf8(&mut res[..]).unwrap().into()) |
| } |
| |
| #[test] |
| fn test_async_flash() { |
| // Creates two block devices for writing raw and sparse image. |
| let sparse_raw = include_bytes!("../../testdata/sparse_test_raw.bin"); |
| let sparse = include_bytes!("../../testdata/sparse_test.bin"); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(vec![0u8; sparse_raw.len() + 67 * 512]); |
| let mut gpt_builder = storage[1].gpt_builder().unwrap(); |
| gpt_builder.add("sparse", [1u8; GPT_GUID_LEN], [1u8; GPT_GUID_LEN], 0, None).unwrap(); |
| block_on(gpt_builder.persist()).unwrap(); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| // "oem gbl-sync-tasks" should return immediately when there is no pending IOs. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-sync-tasks", &resp))).unwrap().is_ok()); |
| // Enable async IO. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-enable-async-task", &resp))).unwrap().is_ok()); |
| |
| // Flashes "boot_a". |
| let expect_boot_a = flipped_bits(include_bytes!("../../../libstorage/test/boot_a.bin")); |
| set_download(&mut gbl_fb, expect_boot_a.as_slice()); |
| block_on(gbl_fb.flash("boot_a", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "block-device", "0:status", "IO pending"); |
| |
| // Flashes the "sparse" partition on the different block device. |
| set_download(&mut gbl_fb, sparse); |
| block_on(gbl_fb.flash("sparse", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "block-device", "1:status", "IO pending"); |
| |
| { |
| // "oem gbl-sync-tasks" should block. |
| let oem_sync_blk_fut = &mut pin!(oem(&mut gbl_fb, "gbl-sync-tasks", &resp)); |
| assert!(poll(oem_sync_blk_fut).is_none()); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| // "oem gbl-sync-tasks" should now be able to finish. |
| assert!(poll(oem_sync_blk_fut).unwrap().is_ok()); |
| } |
| |
| // The two blocks should be in the idle state. |
| check_var(&mut gbl_fb, "block-device", "0:status", "idle"); |
| check_var(&mut gbl_fb, "block-device", "1:status", "idle"); |
| |
| // Verifies flashed image. |
| assert_eq!( |
| fetch(&mut gbl_fb, "boot_a".into(), 0, expect_boot_a.len()).unwrap(), |
| expect_boot_a |
| ); |
| assert_eq!(fetch(&mut gbl_fb, "sparse".into(), 0, sparse_raw.len()).unwrap(), sparse_raw); |
| } |
| |
| #[test] |
| fn test_async_flash_block_on_busy_blk() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| // Enable async IO. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-enable-async-task", &resp))).unwrap().is_ok()); |
| |
| // Flashes boot_a partition. |
| let expect_boot_a = flipped_bits(include_bytes!("../../../libstorage/test/boot_a.bin")); |
| set_download(&mut gbl_fb, expect_boot_a.as_slice()); |
| block_on(gbl_fb.flash("boot_a", &resp)).unwrap(); |
| |
| // Flashes boot_b partition. |
| let expect_boot_b = flipped_bits(include_bytes!("../../../libstorage/test/boot_b.bin")); |
| set_download(&mut gbl_fb, expect_boot_b.as_slice()); |
| { |
| let flash_boot_b_fut = &mut pin!(gbl_fb.flash("boot_b", &resp)); |
| // Previous IO has not completed. Block is busy. |
| assert!(poll(flash_boot_b_fut).is_none()); |
| // There should only be the previous disk IO task for "boot_a". |
| assert_eq!(tasks.borrow_mut().size(), 1); |
| // Schedule the disk IO task for "flash boot_a" to completion. |
| tasks.borrow_mut().run(); |
| // The blocked "flash boot_b" should now be able to finish. |
| assert!(poll(flash_boot_b_fut).is_some()); |
| // There should be a disk IO task spawned for "flash boot_b". |
| assert_eq!(tasks.borrow_mut().size(), 1); |
| // Schedule the disk IO tasks for "flash boot_b" to completion. |
| tasks.borrow_mut().run(); |
| } |
| |
| // Verifies flashed image. |
| assert_eq!( |
| fetch(&mut gbl_fb, "boot_a".into(), 0, expect_boot_a.len()).unwrap(), |
| expect_boot_a |
| ); |
| assert_eq!( |
| fetch(&mut gbl_fb, "boot_b".into(), 0, expect_boot_b.len()).unwrap(), |
| expect_boot_b |
| ); |
| } |
| |
| #[test] |
| #[should_panic( |
| expected = "A Fastboot async task failed: Other(Some(\"test\")), context: flash:boot_a" |
| )] |
| fn test_async_flash_error() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| // Injects an error. |
| storage[0].partition_io(None).unwrap().dev().io().error = |
| liberror::Error::Other(Some("test")).into(); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| // Enable async IO. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-enable-async-task", &resp))).unwrap().is_ok()); |
| // Flashes boot_a partition. |
| let expect_boot_a = flipped_bits(include_bytes!("../../../libstorage/test/boot_a.bin")); |
| set_download(&mut gbl_fb, expect_boot_a.as_slice()); |
| block_on(gbl_fb.flash("boot_a", &resp)).unwrap(); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| } |
| |
| #[test] |
| fn test_async_erase() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"raw_0", [0xaau8; 4096]); |
| storage.add_raw_device(c"raw_1", [0x55u8; 4096]); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| // Enable async IO. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-enable-async-task", &resp))).unwrap().is_ok()); |
| |
| // Erases "raw_0". |
| block_on(gbl_fb.erase("raw_0", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "block-device", "0:status", "IO pending"); |
| |
| // Erases second half of "raw_1" |
| block_on(gbl_fb.erase("raw_1//800", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "block-device", "1:status", "IO pending"); |
| |
| { |
| // "oem gbl-sync-tasks" should block. |
| let oem_sync_blk_fut = &mut pin!(oem(&mut gbl_fb, "gbl-sync-tasks", &resp)); |
| assert!(poll(oem_sync_blk_fut).is_none()); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| // "oem gbl-sync-tasks" should now be able to finish. |
| assert!(poll(oem_sync_blk_fut).unwrap().is_ok()); |
| } |
| |
| // The two blocks should be in the idle state. |
| check_var(&mut gbl_fb, "block-device", "0:status", "idle"); |
| check_var(&mut gbl_fb, "block-device", "1:status", "idle"); |
| |
| assert_eq!(storage[0].partition_io(None).unwrap().dev().io().storage, [0u8; 4096]); |
| assert_eq!( |
| storage[1].partition_io(None).unwrap().dev().io().storage, |
| [[0x55u8; 2048], [0u8; 2048]].concat() |
| ); |
| } |
| |
| #[test] |
| #[should_panic( |
| expected = "A Fastboot async task failed: Other(Some(\"test\")), context: erase:boot_a" |
| )] |
| fn test_async_erase_error() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| // Injects an error. |
| storage[0].partition_io(None).unwrap().dev().io().error = |
| liberror::Error::Other(Some("test")).into(); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| // Enable async IO. |
| assert!(poll(&mut pin!(oem(&mut gbl_fb, "gbl-enable-async-task", &resp))).unwrap().is_ok()); |
| // Erases boot_a partition. |
| block_on(gbl_fb.erase("boot_a", &resp)).unwrap(); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| } |
| |
| #[test] |
| fn test_default_block() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 1]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| let disk_dup = include_bytes!("../../../libstorage/test/gpt_test_2.bin"); |
| storage.add_gpt_device(disk_dup); |
| storage.add_gpt_device(disk_dup); |
| let raw_a = [0xaau8; 4 * 1024]; |
| let raw_b = [0x55u8; 8 * 1024]; |
| storage.add_raw_device(c"raw", raw_a); |
| storage.add_raw_device(c"raw", raw_b); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let resp: TestResponder = Default::default(); |
| |
| let boot_a = include_bytes!("../../../libstorage/test/boot_a.bin"); |
| // Flips the bits on partition "vendor_boot_a" on block device #2 to make it different from |
| // block #1. |
| let vendor_boot_a = |
| flipped_bits(include_bytes!("../../../libstorage/test/vendor_boot_a.bin")); |
| flash_part(&mut gbl_fb, "vendor_boot_a/2", &vendor_boot_a); |
| |
| let size = 512; |
| let off = 512; |
| |
| check_var(&mut gbl_fb, "gbl-default-block", "", "None"); |
| // Sets default block to #2 |
| block_on(oem(&mut gbl_fb, "gbl-set-default-block 2", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "gbl-default-block", "", "0x2"); |
| // The following fetch should succeed and fetch from "vendor_boot_a" on block 2. |
| check_part_upload(&mut gbl_fb, "vendor_boot_a", off, size, None, &vendor_boot_a); |
| |
| // Sets default block to #4 (raw_b) |
| block_on(oem(&mut gbl_fb, "gbl-set-default-block 4", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "gbl-default-block", "", "0x4"); |
| // The following fetch should succeed and fetch from "raw" on block 4. |
| check_part_upload(&mut gbl_fb, "raw", off, size, None, &raw_b); |
| |
| // Fetches with explicit storage ID shouldn't be affected. |
| check_part_upload(&mut gbl_fb, "boot_a", off, size, Some(0), boot_a); |
| check_part_upload(&mut gbl_fb, "raw", off, size, Some(3), &raw_a); |
| check_blk_upload(&mut gbl_fb, 1, off, size, disk_dup); |
| |
| // Fetching without storage ID should use default ID and thus the following should fail. |
| assert!(fetch(&mut gbl_fb, "boot_a".into(), 0, boot_a.len()).is_err()); |
| |
| // Sets default block to #1 (unmodified `disk_dup`) |
| block_on(oem(&mut gbl_fb, "gbl-set-default-block 1", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "gbl-default-block", "", "0x1"); |
| // Fetches whole raw block but without block ID should use the default block. |
| check_part_upload(&mut gbl_fb, "", off, size, None, disk_dup); |
| |
| // Unset default block |
| block_on(oem(&mut gbl_fb, "gbl-unset-default-block", &resp)).unwrap(); |
| check_var(&mut gbl_fb, "gbl-default-block", "", "None"); |
| // Fetching non-unique partitions should now fail. |
| assert!(fetch(&mut gbl_fb, "raw".into(), 0, raw_a.len()).is_err()); |
| assert!(fetch(&mut gbl_fb, "vendor_boot_a".into(), 0, vendor_boot_a.len()).is_err()); |
| assert!(fetch(&mut gbl_fb, "/".into(), 0, 512).is_err()); |
| } |
| |
| #[test] |
| fn test_set_default_block_invalid_arg() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let storage = FakeGblOpsStorage::default(); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let resp: TestResponder = Default::default(); |
| // Missing block device ID. |
| assert!(block_on(oem(&mut gbl_fb, "gbl-set-default-block ", &resp)).is_err()); |
| // Invalid block device ID. |
| assert!(block_on(oem(&mut gbl_fb, "gbl-set-default-block zzz", &resp)).is_err()); |
| // Out of range block device ID. (We've added no block device). |
| assert!(block_on(oem(&mut gbl_fb, "gbl-set-default-block 0", &resp)).is_err()); |
| } |
| |
| #[test] |
| fn test_reboot_sync_all_blocks() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| block_on(oem(&mut gbl_fb, "gbl-enable-async-task", &resp)).unwrap(); |
| |
| // Flashes "boot_a". |
| let expect_boot_a = flipped_bits(include_bytes!("../../../libstorage/test/boot_a.bin")); |
| set_download(&mut gbl_fb, expect_boot_a.as_slice()); |
| block_on(gbl_fb.flash("boot_a", &resp)).unwrap(); |
| // Checks initial state, okay_sent=false. |
| assert!(!(*resp.okay_sent.try_lock().unwrap())); |
| // Performs a reboot. |
| let mut reboot_fut = pin!(gbl_fb.reboot(RebootMode::Normal, &resp)); |
| // There is a pending flash task. Reboot should wait. |
| assert!(poll(&mut reboot_fut).is_none()); |
| assert!(!(*resp.okay_sent.try_lock().unwrap())); |
| assert_eq!(resp.info_messages.try_lock().unwrap()[1], "Syncing storage..."); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| // The reboot can now complete. |
| assert!(poll(&mut reboot_fut).is_some()); |
| assert!((*resp.okay_sent.try_lock().unwrap())); |
| assert_eq!(resp.info_messages.try_lock().unwrap()[2], "Rebooting..."); |
| } |
| |
| #[test] |
| fn test_continue_sync_all_blocks() { |
| let dl_buffers = Shared::from(vec![vec![0u8; 128 * 1024]; 2]); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let tasks = vec![].into(); |
| let parts = gbl_ops.disks(); |
| let mut gbl_fb = |
| GblFastboot::new(&mut gbl_ops, parts, Task::run, &tasks, &dl_buffers, &mut []); |
| let tasks = gbl_fb.tasks(); |
| let resp: TestResponder = Default::default(); |
| |
| block_on(oem(&mut gbl_fb, "gbl-enable-async-task", &resp)).unwrap(); |
| |
| // Flashes "boot_a". |
| let expect_boot_a = flipped_bits(include_bytes!("../../../libstorage/test/boot_a.bin")); |
| set_download(&mut gbl_fb, expect_boot_a.as_slice()); |
| block_on(gbl_fb.flash("boot_a", &resp)).unwrap(); |
| // Performs a continue. |
| let mut continue_fut = pin!(gbl_fb.r#continue(&resp)); |
| // There is a pending flash task. Continue should wait. |
| assert!(poll(&mut continue_fut).is_none()); |
| assert!(!(*resp.okay_sent.try_lock().unwrap())); |
| assert_eq!(resp.info_messages.try_lock().unwrap()[1], "Syncing storage..."); |
| // Schedules the disk IO tasks to completion. |
| tasks.borrow_mut().run(); |
| // The continue can now complete. |
| assert!(poll(&mut continue_fut).is_some()); |
| } |
| |
| /// Generates a length prefixed byte sequence. |
| fn length_prefixed(data: &[u8]) -> Vec<u8> { |
| [&data.len().to_be_bytes()[..], data].concat() |
| } |
| |
| /// Used for a test implementation of [GblUsbTransport] and [GblTcpStream]. |
| #[derive(Default)] |
| struct TestListener { |
| usb_in_queue: VecDeque<Vec<u8>>, |
| usb_out_queue: VecDeque<Vec<u8>>, |
| |
| tcp_in_queue: VecDeque<u8>, |
| tcp_out_queue: VecDeque<u8>, |
| } |
| |
| /// A shared [TestListener]. |
| #[derive(Default)] |
| struct SharedTestListener(Mutex<TestListener>); |
| |
| impl SharedTestListener { |
| /// Locks the listener |
| fn lock(&self) -> MutexGuard<TestListener> { |
| self.0.try_lock().unwrap() |
| } |
| |
| /// Adds packet to USB input |
| fn add_usb_input(&self, packet: &[u8]) { |
| self.lock().usb_in_queue.push_back(packet.into()); |
| } |
| |
| /// Adds bytes to input stream. |
| fn add_tcp_input(&self, data: &[u8]) { |
| self.lock().tcp_in_queue.append(&mut data.to_vec().into()); |
| } |
| |
| /// Adds a length pre-fixed bytes stream. |
| fn add_tcp_length_prefixed_input(&self, data: &[u8]) { |
| self.add_tcp_input(&length_prefixed(data)); |
| } |
| |
| /// Gets a copy of `Self::usb_out_queue`. |
| fn usb_out_queue(&self) -> VecDeque<Vec<u8>> { |
| self.lock().usb_out_queue.clone() |
| } |
| |
| /// Gets a copy of `Self::tcp_out_queue`. |
| fn tcp_out_queue(&self) -> VecDeque<u8> { |
| self.lock().tcp_out_queue.clone() |
| } |
| |
| /// A helper for decoding USB output packets as a string |
| fn dump_usb_out_queue(&self) -> String { |
| let mut res = String::from(""); |
| for v in self.lock().usb_out_queue.iter() { |
| let v = String::from_utf8(v.clone()).unwrap_or(format!("{:?}", v)); |
| res += format!("b{:?},\n", v).as_str(); |
| } |
| res |
| } |
| |
| /// A helper for decoding TCP output data as strings |
| fn dump_tcp_out_queue(&self) -> String { |
| let mut data = self.lock(); |
| let mut v; |
| let (_, mut remains) = data.tcp_out_queue.make_contiguous().split_at(4); |
| let mut res = String::from(""); |
| while !remains.is_empty() { |
| // Parses length-prefixed payload. |
| let (len, rest) = remains.split_first_chunk::<{ size_of::<u64>() }>().unwrap(); |
| (v, remains) = rest.split_at(u64::from_be_bytes(*len).try_into().unwrap()); |
| let s = String::from_utf8(v.to_vec()).unwrap_or(format!("{:?}", v)); |
| res += format!("b{:?},\n", s).as_str(); |
| } |
| res |
| } |
| } |
| |
| impl Transport for &SharedTestListener { |
| async fn receive_packet(&mut self, out: &mut [u8]) -> Result<usize, Error> { |
| match self.lock().usb_in_queue.pop_front() { |
| Some(v) => Ok((&v[..]).read(out).unwrap()), |
| _ => Err(Error::Other(Some("No more data"))), |
| } |
| } |
| |
| async fn send_packet(&mut self, packet: &[u8]) -> Result<(), Error> { |
| Ok(self.lock().usb_out_queue.push_back(packet.into())) |
| } |
| } |
| |
| impl GblUsbTransport for &SharedTestListener { |
| fn has_packet(&mut self) -> bool { |
| !self.lock().usb_in_queue.is_empty() |
| } |
| } |
| |
| impl TcpStream for &SharedTestListener { |
| async fn read_exact(&mut self, out: &mut [u8]) -> Result<(), Error> { |
| match self.lock().tcp_in_queue.read(out).unwrap() == out.len() { |
| true => Ok(()), |
| _ => Err(Error::Other(Some("No more data"))), |
| } |
| } |
| |
| async fn write_exact(&mut self, data: &[u8]) -> Result<(), Error> { |
| Ok(self.lock().tcp_out_queue.append(&mut data.to_vec().into())) |
| } |
| } |
| |
| impl GblTcpStream for &SharedTestListener { |
| fn accept_new(&mut self) -> bool { |
| !self.lock().tcp_in_queue.is_empty() |
| } |
| } |
| |
| /// A helper to make an expected stream of USB output. |
| fn make_expected_usb_out(data: &[&[u8]]) -> VecDeque<Vec<u8>> { |
| VecDeque::from(data.iter().map(|v| v.to_vec()).collect::<Vec<_>>()) |
| } |
| |
| /// A helper to make an expected stream of TCP output. |
| fn make_expected_tcp_out(data: &[&[u8]]) -> VecDeque<u8> { |
| let mut res = VecDeque::<u8>::from(b"FB01".to_vec()); |
| data.iter().for_each(|v| res.append(&mut length_prefixed(v).into())); |
| res |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot() { |
| let storage = FakeGblOpsStorage::default(); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"getvar:version-bootloader"); |
| listener.add_tcp_input(b"FB01"); |
| listener.add_tcp_length_prefixed_input(b"getvar:max-download-size"); |
| listener.add_tcp_length_prefixed_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[b"OKAY1.0"]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| |
| assert_eq!( |
| listener.tcp_out_queue(), |
| make_expected_tcp_out(&[b"OKAY0x20000", b"INFOSyncing storage...", b"OKAY"]), |
| "\nActual TCP output:\n{}", |
| listener.dump_tcp_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_parallel_task() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"raw_0", [0u8; 4 * 1024]); |
| storage.add_raw_device(c"raw_1", [0u8; 8 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| let mut fb_fut = |
| pin!(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| listener.add_usb_input(b"oem gbl-enable-async-task"); |
| listener.add_usb_input(format!("download:{:#x}", 4 * 1024).as_bytes()); |
| listener.add_usb_input(&[0x55u8; 4 * 1024]); |
| listener.add_usb_input(b"flash:raw_0"); |
| |
| listener.add_tcp_input(b"FB01"); |
| listener.add_tcp_length_prefixed_input(format!("download:{:#x}", 8 * 1024).as_bytes()); |
| listener.add_tcp_length_prefixed_input(&[0xaau8; 8 * 1024]); |
| listener.add_tcp_length_prefixed_input(b"flash:raw_1"); |
| |
| assert!(poll_n_times(&mut fb_fut, 100).is_none()); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"OKAY", |
| b"DATA00001000", |
| b"OKAY", |
| b"INFOAn async task is launched. To sync manually, run \"oem gbl-sync-tasks\".", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| |
| assert_eq!( |
| listener.tcp_out_queue(), |
| make_expected_tcp_out(&[ |
| b"DATA00002000", |
| b"OKAY", |
| b"INFOAn async task is launched. To sync manually, run \"oem gbl-sync-tasks\".", |
| b"OKAY", |
| ]), |
| "\nActual TCP output:\n{}", |
| listener.dump_tcp_out_queue() |
| ); |
| |
| // Verifies flashed image on raw_0. |
| assert_eq!(storage[0].partition_io(None).unwrap().dev().io().storage, [0x55u8; 4 * 1024]); |
| |
| // Verifies flashed image on raw_1. |
| assert_eq!(storage[1].partition_io(None).unwrap().dev().io().storage, [0xaau8; 8 * 1024]); |
| } |
| |
| #[test] |
| fn test_oem_add_staged_bootloader_file() { |
| let storage = FakeGblOpsStorage::default(); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.get_zbi_bootloader_files_buffer().unwrap().fill(0); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| // Stages two zbi files. |
| listener.add_usb_input(format!("download:{:#x}", 3).as_bytes()); |
| listener.add_usb_input(b"foo"); |
| listener.add_usb_input(b"oem add-staged-bootloader-file file_1"); |
| listener.add_usb_input(format!("download:{:#x}", 3).as_bytes()); |
| listener.add_usb_input(b"bar"); |
| listener.add_usb_input(b"oem add-staged-bootloader-file file_2"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| let buffer = gbl_ops.get_zbi_bootloader_files_buffer_aligned().unwrap(); |
| let container = ZbiContainer::parse(&buffer[..]).unwrap(); |
| let mut iter = container.iter(); |
| assert_eq!(iter.next().unwrap().payload.as_bytes(), b"\x06file_1foo"); |
| assert_eq!(iter.next().unwrap().payload.as_bytes(), b"\x06file_2bar"); |
| assert!(iter.next().is_none()); |
| } |
| |
| #[test] |
| fn test_oem_add_staged_bootloader_file_missing_file_name() { |
| let storage = FakeGblOpsStorage::default(); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(format!("download:{:#x}", 3).as_bytes()); |
| listener.add_usb_input(b"foo"); |
| listener.add_usb_input(b"oem add-staged-bootloader-file"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"DATA00000003", |
| b"OKAY", |
| b"FAILMissing file name", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ) |
| } |
| |
| #[test] |
| fn test_oem_add_staged_bootloader_file_missing_download() { |
| let storage = FakeGblOpsStorage::default(); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"oem add-staged-bootloader-file file1"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[b"FAILNo file staged", b"INFOSyncing storage...", b"OKAY",]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_fuchsia_fastboot_mdns_packet() { |
| let expected = [ |
| 0x00, 0x00, 0x84, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x09, 0x5f, |
| 0x66, 0x61, 0x73, 0x74, 0x62, 0x6f, 0x6f, 0x74, 0x04, 0x5f, 0x74, 0x63, 0x70, 0x05, |
| 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x00, 0x00, 0x0c, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, |
| 0x00, 0x19, 0x16, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2d, 0x35, 0x32, 0x35, |
| 0x34, 0x2d, 0x30, 0x30, 0x31, 0x32, 0x2d, 0x33, 0x34, 0x35, 0x36, 0xc0, 0x0c, 0xc0, |
| 0x2c, 0x00, 0x21, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x1f, 0x00, 0x00, 0x00, |
| 0x00, 0x15, 0xb2, 0x16, 0x66, 0x75, 0x63, 0x68, 0x73, 0x69, 0x61, 0x2d, 0x35, 0x32, |
| 0x35, 0x34, 0x2d, 0x30, 0x30, 0x31, 0x32, 0x2d, 0x33, 0x34, 0x35, 0x36, 0xc0, 0x1b, |
| 0xc0, 0x57, 0x00, 0x1c, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x10, 0xfe, 0x80, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x54, 0x00, 0xff, 0xfe, 0x12, 0x34, 0x56, |
| ]; |
| let ip6_addr = &[ |
| 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x54, 0x00, 0xff, 0xfe, 0x12, |
| 0x34, 0x56, |
| ]; |
| assert_eq!( |
| fuchsia_fastboot_mdns_packet("fuchsia-5254-0012-3456", ip6_addr).unwrap(), |
| expected |
| ); |
| } |
| |
| #[test] |
| fn test_fuchsia_fastboot_mdns_packet_invalid_node_name() { |
| let ip6_addr = &[ |
| 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x54, 0x00, 0xff, 0xfe, 0x12, |
| 0x34, 0x56, |
| ]; |
| assert!(fuchsia_fastboot_mdns_packet("fuchsia-5254-0012-345", ip6_addr).is_err()); |
| assert!(fuchsia_fastboot_mdns_packet("fuchsia-5254-0012-34567", ip6_addr).is_err()); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt() { |
| let disk_orig = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| // Erase the primary and secondary header. |
| let mut disk = disk_orig.to_vec(); |
| disk[512..][..512].fill(0); |
| disk.last_chunk_mut::<512>().unwrap().fill(0); |
| |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(&disk); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| // Checks that there is no valid partitions for block #0. |
| listener.add_usb_input(b"getvar:partition-size:boot_a"); |
| listener.add_usb_input(b"getvar:partition-size:boot_b"); |
| // No partitions on block #0 should show up in `getvar:all` despite being a GPT device, |
| // since the GPTs are corrupted. |
| listener.add_usb_input(b"getvar:all"); |
| // Download a GPT |
| let gpt = &disk_orig[..34 * 512]; |
| listener.add_usb_input(format!("download:{:#x}", gpt.len()).as_bytes()); |
| listener.add_usb_input(gpt); |
| listener.add_usb_input(b"flash:gpt/0"); |
| // Checks that we can get partition info now. |
| listener.add_usb_input(b"getvar:partition-size:boot_a"); |
| listener.add_usb_input(b"getvar:partition-size:boot_b"); |
| listener.add_usb_input(b"getvar:all"); |
| |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"FAILNotFound", |
| b"FAILNotFound", |
| b"INFOmax-download-size: 0x20000", |
| b"INFOversion-bootloader: 1.0", |
| b"INFOmax-fetch-size: 0xffffffffffffffff", |
| b"INFOblock-device:0:total-blocks: 0x80", |
| b"INFOblock-device:0:block-size: 0x200", |
| b"INFOblock-device:0:status: idle", |
| b"INFOblock-device:1:total-blocks: 0x100", |
| b"INFOblock-device:1:block-size: 0x200", |
| b"INFOblock-device:1:status: idle", |
| b"INFOgbl-default-block: None", |
| b"INFOpartition-size:vendor_boot_a/1: 0x1000", |
| b"INFOpartition-type:vendor_boot_a/1: raw", |
| b"INFOpartition-size:vendor_boot_b/1: 0x1800", |
| b"INFOpartition-type:vendor_boot_b/1: raw", |
| format!("INFO{}:1: {}:1", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_bytes(), |
| format!("INFO{}:2: {}:2", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_bytes(), |
| b"OKAY", |
| b"DATA00004400", |
| b"OKAY", |
| b"INFOUpdating GPT...", |
| b"OKAY", |
| b"OKAY0x2000", |
| b"OKAY0x3000", |
| b"INFOmax-download-size: 0x20000", |
| b"INFOversion-bootloader: 1.0", |
| b"INFOmax-fetch-size: 0xffffffffffffffff", |
| b"INFOblock-device:0:total-blocks: 0x80", |
| b"INFOblock-device:0:block-size: 0x200", |
| b"INFOblock-device:0:status: idle", |
| b"INFOblock-device:1:total-blocks: 0x100", |
| b"INFOblock-device:1:block-size: 0x200", |
| b"INFOblock-device:1:status: idle", |
| b"INFOgbl-default-block: None", |
| b"INFOpartition-size:boot_a/0: 0x2000", |
| b"INFOpartition-type:boot_a/0: raw", |
| b"INFOpartition-size:boot_b/0: 0x3000", |
| b"INFOpartition-type:boot_b/0: raw", |
| b"INFOpartition-size:vendor_boot_a/1: 0x1000", |
| b"INFOpartition-type:vendor_boot_a/1: raw", |
| b"INFOpartition-size:vendor_boot_b/1: 0x1800", |
| b"INFOpartition-type:vendor_boot_b/1: raw", |
| format!("INFO{}:1: {}:1", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_bytes(), |
| format!("INFO{}:2: {}:2", FakeGblOps::GBL_TEST_VAR, FakeGblOps::GBL_TEST_VAR_VAL) |
| .as_bytes(), |
| b"OKAY", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt_resize() { |
| let disk_orig = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let mut disk = disk_orig.to_vec(); |
| // Doubles the size of the disk |
| disk.resize(disk_orig.len() * 2, 0); |
| |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| storage.add_gpt_device(&disk); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| // Checks current size of last partition `boot_b`. |
| listener.add_usb_input(b"getvar:partition-size:boot_b"); |
| // Sets a default block. |
| listener.add_usb_input(b"oem gbl-set-default-block 1"); |
| let gpt = &disk_orig[..34 * 512]; |
| listener.add_usb_input(format!("download:{:#x}", gpt.len()).as_bytes()); |
| listener.add_usb_input(gpt); |
| // No need to specify block device index |
| listener.add_usb_input(b"flash:gpt//resize"); |
| // Checks updated size of last partition `boot_b`. |
| listener.add_usb_input(b"getvar:partition-size:boot_b"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"OKAY0x3000", |
| b"INFODefault block device: 0x1", |
| b"OKAY", |
| b"DATA00004400", |
| b"OKAY", |
| b"INFOUpdating GPT...", |
| b"OKAY", |
| b"OKAY0x15a00", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt_no_downloaded_gpt() { |
| let disk = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(&disk); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"flash:gpt/0"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[b"FAILNo GPT downloaded", b"INFOSyncing storage...", b"OKAY",]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt_bad_gpt() { |
| let disk = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(&disk); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| // Download a bad GPT. |
| let mut gpt = disk[..34 * 512].to_vec(); |
| gpt[512] = !gpt[512]; |
| listener.add_usb_input(format!("download:{:#x}", gpt.len()).as_bytes()); |
| listener.add_usb_input(&gpt); |
| listener.add_usb_input(b"flash:gpt/0"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"DATA00004400", |
| b"OKAY", |
| b"INFOUpdating GPT...", |
| b"FAILGptError(\n IncorrectMagic(\n 6075990659671082682,\n ),\n)", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt_invalid_input() { |
| let disk_orig = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(&disk_orig); |
| storage.add_gpt_device(&disk_orig); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| let gpt = &disk_orig[..34 * 512]; |
| listener.add_usb_input(format!("download:{:#x}", gpt.len()).as_bytes()); |
| listener.add_usb_input(gpt); |
| // Missing block device ID. |
| listener.add_usb_input(b"flash:gpt"); |
| // Out of range block device ID. |
| listener.add_usb_input(b"flash:gpt/2"); |
| // Invalid option. |
| listener.add_usb_input(b"flash:gpt/0/invalid-arg"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"DATA00004400", |
| b"OKAY", |
| b"FAILBlock ID is required for flashing GPT", |
| b"FAILInvalid block ID", |
| b"FAILUnknown argument", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_update_gpt_fail_on_raw_blk() { |
| let disk_orig = include_bytes!("../../../libstorage/test/gpt_test_1.bin"); |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"raw_0", [0u8; 1024 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| let gpt = &disk_orig[..34 * 512]; |
| listener.add_usb_input(format!("download:{:#x}", gpt.len()).as_bytes()); |
| listener.add_usb_input(gpt); |
| listener.add_usb_input(b"flash:gpt/0"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"DATA00004400", |
| b"OKAY", |
| b"INFOUpdating GPT...", |
| b"FAILBlock device is not for GPT", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_erase_gpt() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_1.bin")); |
| storage.add_gpt_device(include_bytes!("../../../libstorage/test/gpt_test_2.bin")); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| // Erases the GPT on disk #0. |
| listener.add_usb_input(b"erase:gpt/0"); |
| // Checks that we can no longer get partition info on disk #0. |
| listener.add_usb_input(b"getvar:partition-size:boot_a"); |
| listener.add_usb_input(b"getvar:partition-size:boot_b"); |
| // Checks that we can still get partition info on disk #1. |
| listener.add_usb_input(b"getvar:partition-size:vendor_boot_a"); |
| listener.add_usb_input(b"getvar:partition-size:vendor_boot_b"); |
| listener.add_usb_input(b"continue"); |
| |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"OKAY", |
| b"FAILNotFound", |
| b"FAILNotFound", |
| b"OKAY0x1000", |
| b"OKAY0x1800", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_oem_erase_gpt_fail_on_raw_blk() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"raw_0", [0u8; 1024 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"erase:gpt/0"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"FAILBlock device is not for GPT", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| /// Helper for testing fastboot set_active in fuchsia A/B/R mode. |
| fn test_run_gbl_fastboot_set_active_fuchsia_abr(cmd: &str, slot: SlotIndex) { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.os = Some(Os::Fuchsia); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| mark_slot_unbootable(&mut GblAbrOps(&mut gbl_ops), SlotIndex::A).unwrap(); |
| mark_slot_unbootable(&mut GblAbrOps(&mut gbl_ops), SlotIndex::B).unwrap(); |
| |
| // Flash some data to `durable_boot` after A/B/R metadata. This is for testing that sync |
| // storage is done first. |
| let data = vec![0x55u8; 4 * 1024 - ABR_DATA_SIZE]; |
| listener.add_usb_input(b"oem gbl-enable-async-task"); |
| listener.add_usb_input(format!("download:{:#x}", 4 * 1024 - ABR_DATA_SIZE).as_bytes()); |
| listener.add_usb_input(&data); |
| listener.add_usb_input(format!("flash:durable_boot//{:#x}", ABR_DATA_SIZE).as_bytes()); |
| // Issues set_active commands |
| listener.add_usb_input(cmd.as_bytes()); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"OKAY", |
| b"DATA00000fe0", |
| b"OKAY", |
| b"INFOAn async task is launched. To sync manually, run \"oem gbl-sync-tasks\".", |
| b"OKAY", |
| b"OKAY", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| |
| assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (slot, false)); |
| // Verifies storage sync |
| assert_eq!( |
| storage[0].partition_io(None).unwrap().dev().io().storage[ABR_DATA_SIZE..], |
| data |
| ); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_set_active_fuchsia_abr_a() { |
| test_run_gbl_fastboot_set_active_fuchsia_abr("set_active:a", SlotIndex::A); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_set_active_fuchsia_abr_b() { |
| test_run_gbl_fastboot_set_active_fuchsia_abr("set_active:b", SlotIndex::B); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_set_active_fuchsia_abr_invalid_slot() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.os = Some(Os::Fuchsia); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"set_active:r"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"FAILInvalid slot index for Fuchsia A/B/R", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_fuchsia_reboot_bootloader_abr() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.os = Some(Os::Fuchsia); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"reboot-bootloader"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"INFOSyncing storage...", |
| b"INFORebooting to bootloader...", |
| b"OKAY", |
| b"FAILUnknown", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| |
| assert_eq!(get_and_clear_one_shot_bootloader(&mut GblAbrOps(&mut gbl_ops)), Ok(true)); |
| } |
| |
| #[test] |
| fn test_run_gbl_fastboot_fuchsia_reboot_recovery_abr() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.os = Some(Os::Fuchsia); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(b"reboot-recovery"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"INFOSyncing storage...", |
| b"INFORebooting to recovery...", |
| b"OKAY", |
| b"FAILUnknown", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| |
| // One shot recovery is set. |
| assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::R, false)); |
| assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::A, false)); |
| } |
| |
| #[test] |
| fn test_legacy_fvm_partition_aliase() { |
| let mut storage = FakeGblOpsStorage::default(); |
| storage.add_raw_device(c"fuchsia-fvm", [0x00u8; 4 * 1024]); |
| let buffers = vec![vec![0u8; 128 * 1024]; 2]; |
| let mut gbl_ops = FakeGblOps::new(&storage); |
| gbl_ops.os = Some(Os::Fuchsia); |
| let listener: SharedTestListener = Default::default(); |
| let (usb, tcp) = (&listener, &listener); |
| |
| listener.add_usb_input(format!("download:{:#x}", 4 * 1024).as_bytes()); |
| listener.add_usb_input(&[0xaau8; 4 * 1024]); |
| listener.add_usb_input(b"flash:fvm"); |
| listener.add_usb_input(b"continue"); |
| block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp), &mut [])); |
| |
| assert_eq!( |
| listener.usb_out_queue(), |
| make_expected_usb_out(&[ |
| b"DATA00001000", |
| b"OKAY", |
| b"OKAY", |
| b"INFOSyncing storage...", |
| b"OKAY", |
| ]), |
| "\nActual USB output:\n{}", |
| listener.dump_usb_out_queue() |
| ); |
| } |
| } |