blob: d589f2d7303b6895ff8c3b741446f10f685725fd [file] [log] [blame]
// 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.
//! This library provides APIs for receiving, processing and replying to fastboot commands. To use
//! the library:
//!
//! 1. Provide a transport backend by implementing the `Transport` trait.
//!
//! ```
//!
//! struct FastbootTransport {}
//!
//! impl Transport<MyErrorType> for TestTransport {
//! fn receive_packet(&mut self, out: &mut [u8]) -> Result<usize, TransportError> {
//! todo!();
//! }
//!
//! fn send_packet(&mut self, packet: &[u8]) -> Result<(), TransportError> {
//! todo!();
//! }
//! }
//! ```
//!
//! 2. Provide a fastboot command backend by implementing the `FastbootImplementation` trait.
//! i.e.
//!
//! ```
//!
//! struct FastbootCommand {}
//!
//! impl FastbootImplementation for FastbootTest {
//! fn get_var(
//! &mut self,
//! var: &str,
//! args: Split<char>,
//! out: &mut [u8],
//! ) -> CommandResult<usize> {
//! todo!();
//! }
//!
//! ...
//! }
//!```
//!
//! 3. Construct a `Fastboot` object with a given download buffer. Pass the transport, command
//! implementation and call the `run()` method:
//!
//! ```
//! let mut fastboot_impl: FastbootCommand = ...;
//! let mut transport: TestTransport = ...;
//! let download_buffer: &mut [u8] = ...;
//! let mut fastboot = Fastboot::new();
//! let result = run(&mut transport, &mut fastboot_impl, &[]);
//! ```
#![cfg_attr(not(test), no_std)]
#![allow(async_fn_in_trait)]
use core::{
ffi::CStr,
fmt::{Debug, Display, Formatter, Write},
str::{from_utf8, Split},
};
use gbl_async::{block_on, yield_now};
use liberror::{Error, Result};
use libutils::{snprintf, FormattedBytes};
/// Local session module
pub mod local_session;
/// Maximum packet size that can be accepted from the host.
///
/// The transport layer may have its own size limits that reduce the packet size further.
pub const MAX_COMMAND_SIZE: usize = 4096;
/// Maximum packet size that will be sent to the host.
///
/// The `fastboot` host tool originally had a 64-byte packet size max, but this was increased
/// to 256 in 2020, so any reasonably recent host binary should be able to support 256.
///
/// The transport layer may have its own size limits that reduce the packet size further.
pub const MAX_RESPONSE_SIZE: usize = 256;
/// Trait to provide the transport layer for a fastboot implementation.
///
/// Fastboot supports these transports:
/// * USB
/// * TCP
/// * UDP
pub trait Transport {
/// Fetches the next fastboot packet into `out`.
///
/// Returns the actual size of the packet on success.
///
/// TODO(b/322540167): In the future, we may want to support using `[MaybeUninit<u8>]` as the
/// download buffer to avoid expensive initialization at the beginning. This would require an
/// interface where the implementation provides the buffer for us to copy instead of us.
async fn receive_packet(&mut self, out: &mut [u8]) -> Result<usize>;
/// Sends a fastboot packet.
///
/// The method assumes `packet` is sent or at least copied to queue after it returns, where
/// the buffer can go out of scope without affecting anything.
async fn send_packet(&mut self, packet: &[u8]) -> Result<()>;
}
/// For now, we hardcode the expected version, until we need to distinguish between multiple
/// versions.
const TCP_HANDSHAKE_MESSAGE: &[u8] = b"FB01";
/// A trait representing a TCP stream reader/writer. Fastboot over TCP has additional handshake
/// process and uses a length-prefixed wire message format. It is recommended that caller
/// implements this trait instead of `Transport`, and uses the API `Fastboot::run_tcp_session()`
/// to perform fastboot over TCP. It internally handles handshake and wire message parsing.
pub trait TcpStream {
/// Reads to `out` for exactly `out.len()` number bytes from the TCP connection.
async fn read_exact(&mut self, out: &mut [u8]) -> Result<()>;
/// Sends exactly `data.len()` number bytes from `data` to the TCP connection.
async fn write_exact(&mut self, data: &[u8]) -> Result<()>;
}
/// Implements [Transport] on a [TcpStream].
pub struct TcpTransport<'a, T: TcpStream>(&'a mut T);
impl<'a, T: TcpStream> TcpTransport<'a, T> {
/// Creates an instance from a newly connected TcpStream and performs handshake.
pub fn new_and_handshake(tcp_stream: &'a mut T) -> Result<Self> {
let mut handshake = [0u8; 4];
block_on(tcp_stream.write_exact(TCP_HANDSHAKE_MESSAGE))?;
block_on(tcp_stream.read_exact(&mut handshake[..]))?;
match handshake == *TCP_HANDSHAKE_MESSAGE {
true => Ok(Self(tcp_stream)),
_ => Err(Error::InvalidHandshake),
}
}
}
impl<'a, T: TcpStream> Transport for TcpTransport<'a, T> {
async fn receive_packet(&mut self, out: &mut [u8]) -> Result<usize> {
let mut length_prefix = [0u8; 8];
self.0.read_exact(&mut length_prefix[..]).await?;
let packet_size: usize = u64::from_be_bytes(length_prefix).try_into()?;
match out.len() < packet_size {
true => Err(Error::InvalidInput),
_ => {
self.0.read_exact(&mut out[..packet_size]).await?;
Ok(packet_size)
}
}
}
async fn send_packet(&mut self, packet: &[u8]) -> Result<()> {
self.0.write_exact(&mut u64::try_from(packet.len())?.to_be_bytes()[..]).await?;
self.0.write_exact(packet).await
}
}
const COMMAND_ERROR_LENGTH: usize = MAX_RESPONSE_SIZE - 4;
/// `CommandError` is the return error type for methods in trait `FastbootImplementation` when
/// they fail. It will be converted into string and sent as fastboot error message "FAIL<string>".
///
/// Any type that implements `Display` trait can be converted into it. However, because fastboot
/// response message is limited to `MAX_RESPONSE_SIZE`. If the final displayed string length
/// exceeds it, the rest of the content is ignored.
pub struct CommandError(FormattedBytes<[u8; COMMAND_ERROR_LENGTH]>);
impl CommandError {
/// Converts to string.
pub fn to_str(&self) -> &str {
self.0.to_str()
}
/// Clones the error.
pub fn clone(&self) -> Self {
self.to_str().into()
}
}
impl Debug for CommandError {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.to_str())
}
}
impl<T: Display> From<T> for CommandError {
fn from(val: T) -> Self {
let mut res = CommandError(FormattedBytes::new([0u8; COMMAND_ERROR_LENGTH]));
write!(res.0, "{}", val).unwrap();
res
}
}
/// Type alias for Result that wraps a CommandError
pub type CommandResult<T> = core::result::Result<T, CommandError>;
/// Fastboot reboot mode
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum RebootMode {
/// "fastboot reboot". Normal reboot.
Normal,
/// "fastboot reboot-bootloader". Reboot to bootloader.
Bootloader,
/// "fastboot reboot-fastboot". Reboot to userspace fastboot.
Fastboot,
/// "fastboot reboot-recovery". Reboot to recovery.
Recovery,
}
/// Implementation for Fastboot command backends.
pub trait FastbootImplementation {
/// Backend for `fastboot getvar ...`
///
/// Gets the value of a variable specified by name and configuration represented by list of
/// additional arguments in `args`.
///
/// Variable `max-download-size`, `version` are reserved by the library.
///
/// # Args
///
/// * `var`: Name of the variable.
/// * `args`: Additional arguments.
/// * `out`: Output buffer for storing the variable value.
/// * `responder`: An instance of `InfoSender`.
///
/// TODO(b/322540167): Figure out other reserved variables.
async fn get_var(
&mut self,
var: &CStr,
args: impl Iterator<Item = &'_ CStr> + Clone,
out: &mut [u8],
responder: impl InfoSender,
) -> CommandResult<usize>;
/// A helper API for getting the value of a fastboot variable and decoding it into string.
async fn get_var_as_str<'s>(
&mut self,
var: &CStr,
args: impl Iterator<Item = &'_ CStr> + Clone,
responder: impl InfoSender,
out: &'s mut [u8],
) -> CommandResult<&'s str> {
let size = self.get_var(var, args, out, responder).await?;
Ok(from_utf8(out.get(..size).ok_or("Invalid variable size")?)
.map_err(|_| "Value is not string")?)
}
/// Backend for `fastboot getvar all`.
///
/// Iterates all combinations of fastboot variable, configurations and values that need to be
/// included in the response to `fastboot getvar all`.
///
/// # Args
///
/// * `responder`: An implementation VarInfoSender. Implementation should call
/// `VarInfoSender::send` for all combinations of Fastboot variable/argument/value that needs
/// to be included in the response to `fastboot getvarl all`:
///
/// async fn get_var_all(&mut self, f: F, resp: impl VarInfoSender)
/// -> CommandResult<()> {
/// resp.send("partition-size", &["boot_a"], /* size of boot_a */).await?;
/// resp.send("partition-size", &["boot_b"], /* size of boot_b */).await?;
/// resp.send("partition-size", &["init_boot_a"], /* size of init_boot_a */).await?;
/// resp.send("partition-size", &["init_boot_b"], /* size of init_boot_b */).await?;
/// Ok(())
/// }
///
/// will generates the following outputs for `fastboot getvar all`:
///
/// ...
/// (bootloader) partition-size:boot_a: <size of boot_a>
/// (bootloader) partition-size:boot_b: <size of boot_b>
/// (bootloader) partition-size:init_boot_a: <size of init_boot_a>
/// (bootloader) partition-size:init_boot_b: <size of init_boot_b>
/// ...
///
/// TODO(b/322540167): This and `get_var()` contain duplicated logic. Investigate if there can
/// be better solutions for doing the combination traversal.
async fn get_var_all(&mut self, responder: impl VarInfoSender) -> CommandResult<()>;
/// Backend for getting download buffer
async fn get_download_buffer(&mut self) -> &mut [u8];
/// Called when a download is completed.
async fn download_complete(
&mut self,
download_size: usize,
responder: impl InfoSender,
) -> CommandResult<()>;
/// Backend for `fastboot flash ...`
///
/// # Args
///
/// * `part`: Name of the partition.
/// * `responder`: An instance of `InfoSender`.
async fn flash(&mut self, part: &str, responder: impl InfoSender) -> CommandResult<()>;
/// Backend for `fastboot erase ...`
///
/// # Args
///
/// * `part`: Name of the partition.
/// * `responder`: An instance of `InfoSender`.
async fn erase(&mut self, part: &str, responder: impl InfoSender) -> CommandResult<()>;
/// Backend for `fastboot get_staged ...`
///
/// # Args
///
/// * `responder`: An instance of `UploadBuilder + InfoSender` for initiating and uploading
/// data. For example:
///
/// ```
/// async fn upload(
/// &mut self,
/// responder: impl UploadBuilder + InfoSender,
/// ) -> CommandResult<()> {
/// let data = ..;
/// // Sends a total of 1024 bytes data.
/// responder.send_info("About to upload...").await?;
/// let mut uploader = responder.initiate_upload(1024).await?;
/// // Can upload in multiple batches.
/// uploader.upload(&data[..512]).await?;
/// uploader.upload(&data[512..]).await?;
/// Ok(())
/// }
/// ```
///
/// If implementation fails to upload enough, or attempts to upload more than expected data
/// with `Uploader::upload()`, an error will be returned.
async fn upload(&mut self, responder: impl UploadBuilder + InfoSender) -> CommandResult<()>;
/// Backend for `fastboot fetch ...`
///
/// # Args
///
/// * `part`: The partition name.
/// * `offset`: The offset into the partition for upload.
/// * `size`: The number of bytes to upload.
/// * `responder`: An instance of `UploadBuilder + InfoSender` for initiating and uploading data.
async fn fetch(
&mut self,
part: &str,
offset: u64,
size: u64,
responder: impl UploadBuilder + InfoSender,
) -> CommandResult<()>;
/// Backend for `fastboot reboot/reboot-bootloader/reboot-fastboot/reboot-recovery`
///
/// # Args
///
/// * `mode`: An `RebootMode` specifying the reboot mode.
/// * `responder`: An instance of `InfoSender + OkaySender`. Implementation should call
/// `responder.send_okay("")` right before reboot to notify the remote host that the
/// operation is successful.
///
/// # Returns
///
/// * The method is not expected to return if reboot is successful.
/// * Returns `Err(e)` on error.
async fn reboot(
&mut self,
mode: RebootMode,
responder: impl InfoSender + OkaySender,
) -> CommandError;
/// Method for handling `fastboot continue` clean up.
///
/// `run()` and `run_tcp_session()` exit after receiving `fastboot continue.` The method is for
/// implementation to perform necessary clean up.
///
/// # Args
///
/// * `responder`: An instance of `InfoSender`.
async fn r#continue(&mut self, responder: impl InfoSender) -> CommandResult<()>;
/// Backend for `fastboot set_active`.
async fn set_active(&mut self, slot: &str, responder: impl InfoSender) -> CommandResult<()>;
/// Backend for `fastboot boot`
///
/// # Args
///
/// * `responder`: An instance of `InfoSender + OkaySender`. Implementation should call
/// `responder.send_okay("")` right before boot to notify the remote host that the
/// operation is successful.
///
/// # Returns
///
/// * The method is always return OK to let fastboot continue.
/// * Returns `Err(e)` on error.
async fn boot(&mut self, responder: impl InfoSender + OkaySender) -> CommandResult<()>;
/// Backend for `fastboot oem ...`.
///
/// # Args
///
/// * `cmd`: The OEM command string that comes after "oem ".
/// * `responder`: An instance of `InfoSender`.
/// * `res`: The responder buffer. Upon success, implementation can use the buffer to
/// construct a valid UTF8 string which will be sent as "OKAY<string>"
///
/// # Returns
///
/// On success, returns the portion of `res` used by the construction of string message.
async fn oem<'a>(
&mut self,
cmd: &str,
responder: impl InfoSender,
res: &'a mut [u8],
) -> CommandResult<&'a [u8]>;
// TODO(b/322540167): Add methods for other commands.
}
/// An internal convenient macro helper for `fastboot_okay`, `fastboot_fail` and `fastboot_info`.
macro_rules! fastboot_msg {
( $arr:expr, $msg_type:expr, $( $x:expr ),* $(,)? ) => {
{
let mut formatted_bytes = FormattedBytes::new(&mut $arr[..]);
write!(formatted_bytes, $msg_type).unwrap();
write!(formatted_bytes, $($x,)*).unwrap();
let size = formatted_bytes.size();
&mut $arr[..size]
}
};
}
/// An internal convenient macro that constructs a formatted fastboot OKAY message.
macro_rules! fastboot_okay {
( $arr:expr, $( $x:expr ),* $(,)?) => { fastboot_msg!($arr, "OKAY", $($x,)*) };
}
/// An internal convenient macro that constructs a formatted fastboot FAIL message.
macro_rules! fastboot_fail {
( $arr:expr, $( $x:expr ),* $(,)?) => { fastboot_msg!($arr, "FAIL", $($x,)*) };
}
/// `VarInfoSender` provide an interface for sending variable/args/value combination during the
/// processing of `fastboot getvar all`
pub trait VarInfoSender {
/// Send a combination of variable name, arguments and value.
///
/// The method sends a fastboot message "INFO<var>:<args>:<val>" to the host.
///
/// # Args
///
/// * `name`: Name of the fastboot variable.
/// * `args`: An iterator to additional arguments.
/// * `val`: Value of the variable.
async fn send_var_info(
&mut self,
name: &str,
args: impl IntoIterator<Item = &'_ str>,
val: &str,
) -> Result<()>;
}
/// Provides an API for sending fastboot INFO messages.
pub trait InfoSender {
/// Sends formatted INFO message.
///
/// # Args:
///
/// * `cb`: A closure provided by the caller for constructing the formatted messagae.
async fn send_formatted_info<F: FnOnce(&mut dyn Write)>(&mut self, cb: F) -> Result<()>;
/// Sends a Fastboot "INFO<`msg`>" packet.
async fn send_info(&mut self, msg: &str) -> Result<()> {
self.send_formatted_info(|w| write!(w, "{}", msg).unwrap()).await
}
}
/// Provides an API for sending fastboot OKAY messages.
pub trait OkaySender {
/// Sends formatted Okay message.
///
/// # Args:
///
/// * `cb`: A closure provided by the caller for constructing the formatted messagae.
async fn send_formatted_okay<F: FnOnce(&mut dyn Write)>(self, cb: F) -> Result<()>;
/// Sends a fastboot OKAY<msg> packet. `Self` is consumed.
async fn send_okay(self, msg: &str) -> Result<()>
where
Self: Sized,
{
self.send_formatted_okay(|w| write!(w, "{}", msg).unwrap()).await
}
}
/// `UploadBuilder` provides API for initiating a fastboot upload.
pub trait UploadBuilder {
/// Starts the upload.
///
/// In a real fastboot context, the method should send `DATA0xXXXXXXXX` to the remote host to
/// start the download. An `Uploader` implementation should be returned for uploading payload.
async fn initiate_upload(self, data_size: u64) -> Result<impl Uploader>;
}
/// `UploadBuilder` provides API for uploading payload.
pub trait Uploader {
/// Uploads data to the Fastboot host.
async fn upload(&mut self, data: &[u8]) -> Result<()>;
}
/// `Responder` implements APIs for fastboot backend to send fastboot messages and uploading data.
struct Responder<'a, T: Transport> {
buffer: [u8; MAX_RESPONSE_SIZE],
transport: &'a mut T,
transport_error: Result<()>,
remaining_upload: u64,
}
impl<'a, T: Transport> Responder<'a, T> {
fn new(transport: &'a mut T) -> Self {
Self {
buffer: [0u8; MAX_RESPONSE_SIZE],
transport,
transport_error: Ok(()),
remaining_upload: 0,
}
}
/// A helper for sending a fastboot message in the buffer.
async fn send_buffer(&mut self, size: usize) -> Result<()> {
self.transport_error?;
assert!(size < self.buffer.len());
self.transport_error = self.transport.send_packet(&self.buffer[..size]).await;
Ok(self.transport_error?)
}
/// Helper for sending a formatted fastboot message.
///
/// # Args:
///
/// * `cb`: A closure provided by the caller for constructing the formatted messagae.
async fn send_formatted_msg<F: FnOnce(&mut dyn Write)>(
&mut self,
msg_type: &str,
cb: F,
) -> Result<()> {
let mut formatted_bytes = FormattedBytes::new(&mut self.buffer);
write!(formatted_bytes, "{}", msg_type).unwrap();
cb(&mut formatted_bytes);
let size = formatted_bytes.size();
self.send_buffer(size).await
}
/// Sends a fastboot DATA message.
async fn send_data_message(&mut self, data_size: u64) -> Result<()> {
self.send_formatted_msg("DATA", |v| write!(v, "{:08x}", data_size).unwrap()).await
}
}
impl<'a, T: Transport> VarInfoSender for &mut Responder<'a, T> {
async fn send_var_info(
&mut self,
name: &str,
args: impl IntoIterator<Item = &'_ str>,
val: &str,
) -> Result<()> {
// Sends a "INFO<var>:<':'-separated args>:<val>" packet to the host.
Ok(self
.send_formatted_msg("INFO", |v| {
write!(v, "{}", name).unwrap();
args.into_iter().for_each(|arg| write!(v, ":{}", arg).unwrap());
write!(v, ": {}", val).unwrap();
})
.await?)
}
}
/// An internal convenient macro that sends a formatted fastboot OKAY message via a `Responder`
macro_rules! reply_okay {
( $resp:expr, $( $x:expr ),* $(,)?) => {
{
let len = fastboot_okay!($resp.buffer, $($x,)*).len();
$resp.send_buffer(len).await
}
};
}
/// An internal convenient macro that sends a formatted fastboot FAIL message via a `Responder`
macro_rules! reply_fail {
( $resp:expr, $( $x:expr ),* $(,)?) => {
{
let len = fastboot_fail!($resp.buffer, $($x,)*).len();
$resp.send_buffer(len).await
}
};
}
impl<T: Transport> InfoSender for &mut Responder<'_, T> {
async fn send_formatted_info<F: FnOnce(&mut dyn Write)>(&mut self, cb: F) -> Result<()> {
Ok(self.send_formatted_msg("INFO", cb).await?)
}
}
impl<T: Transport> OkaySender for &mut Responder<'_, T> {
async fn send_formatted_okay<F: FnOnce(&mut dyn Write)>(self, cb: F) -> Result<()> {
Ok(self.send_formatted_msg("OKAY", cb).await?)
}
}
impl<'a, T: Transport> UploadBuilder for &mut Responder<'a, T> {
async fn initiate_upload(self, data_size: u64) -> Result<impl Uploader> {
self.send_data_message(data_size).await?;
self.remaining_upload = data_size;
Ok(self)
}
}
impl<'a, T: Transport> Uploader for &mut Responder<'a, T> {
/// Uploads data. Returns error if accumulative amount exceeds `data_size` passed to
/// `UploadBuilder::start()`.
async fn upload(&mut self, data: &[u8]) -> Result<()> {
self.transport_error?;
self.remaining_upload = self
.remaining_upload
.checked_sub(data.len().try_into().map_err(|_| "")?)
.ok_or(Error::Other(Some("Invalid size of upload data")))?;
self.transport_error = self.transport.send_packet(data).await;
Ok(())
}
}
pub mod test_utils {
//! Test utilities to help users of this library write unit tests.
use crate::{InfoSender, UploadBuilder, Uploader};
use core::fmt::Write;
use liberror::Error;
/// A test implementation of `UploadBuilder` for unittesting
/// `FastbootImplementation::upload()`.
///
/// The test uploader simply uploads to a user provided buffer.
pub struct TestUploadBuilder<'a>(pub &'a mut [u8]);
impl<'a> UploadBuilder for TestUploadBuilder<'a> {
async fn initiate_upload(self, _: u64) -> Result<impl Uploader, Error> {
Ok(TestUploader(0, self.0))
}
}
impl<'a> InfoSender for TestUploadBuilder<'a> {
async fn send_formatted_info<F: FnOnce(&mut dyn Write)>(
&mut self,
_: F,
) -> Result<(), Error> {
// Not needed currently.
Ok(())
}
}
// (Bytes sent, upload buffer)
struct TestUploader<'a>(usize, &'a mut [u8]);
impl Uploader for TestUploader<'_> {
async fn upload(&mut self, data: &[u8]) -> Result<(), Error> {
self.1[self.0..][..data.len()].clone_from_slice(data);
self.0 = self.0.checked_add(data.len()).unwrap();
Ok(())
}
}
}
const MAX_DOWNLOAD_SIZE_NAME: &'static str = "max-download-size";
/// Converts a null-terminated command line string where arguments are separated by ':' into an
/// iterator of individual argument as CStr.
fn cmd_to_c_string_args(cmd: &mut [u8]) -> impl Iterator<Item = &CStr> + Clone {
let end = cmd.iter().position(|v| *v == 0).unwrap();
// Replace ':' with NULL.
cmd.iter_mut().filter(|v| **v == b':').for_each(|v| *v = 0);
cmd[..end + 1].split_inclusive(|v| *v == 0).map(|v| CStr::from_bytes_until_nul(v).unwrap())
}
/// Helper for handling "fastboot getvar ..."
async fn get_var(
cmd: &mut [u8],
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let mut args = cmd_to_c_string_args(cmd).skip(1);
let Some(var) = args.next() else {
return reply_fail!(resp, "Missing variable");
};
match var.to_str()? {
"all" => return get_var_all(transport, fb_impl).await,
MAX_DOWNLOAD_SIZE_NAME => {
return reply_okay!(resp, "{:#x}", fb_impl.get_download_buffer().await.len());
}
_ => {
let mut val = [0u8; MAX_RESPONSE_SIZE];
match fb_impl.get_var_as_str(var, args, &mut resp, &mut val[..]).await {
Ok(s) => reply_okay!(resp, "{}", s),
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
}
}
/// A wrapper of `get_var_all()` that first iterates reserved variables.
async fn get_var_all_with_native(
fb_impl: &mut impl FastbootImplementation,
mut sender: impl VarInfoSender,
) -> CommandResult<()> {
// Process the built-in MAX_DOWNLOAD_SIZE_NAME variable.
let mut size_str = [0u8; 32];
let size_str = snprintf!(size_str, "{:#x}", fb_impl.get_download_buffer().await.len());
sender.send_var_info(MAX_DOWNLOAD_SIZE_NAME, [], size_str).await?;
fb_impl.get_var_all(sender).await
}
/// Method for handling "fastboot getvar all"
async fn get_var_all(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
// Don't allow custom INFO messages because variable values are sent as INFO messages.
let get_res = get_var_all_with_native(fb_impl, &mut resp).await;
match get_res {
Ok(()) => reply_okay!(resp, ""),
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
/// Helper for handling "fastboot download:...".
async fn download(
mut args: Split<'_, char>,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let total_download_size = match (|| -> CommandResult<usize> {
usize::try_from(next_arg_u64(&mut args)?.ok_or("Not enough argument")?)
.map_err(|_| "Download size overflow".into())
})() {
Err(e) => return reply_fail!(resp, "{}", e.to_str()),
Ok(v) => v,
};
let download_buffer = &mut fb_impl.get_download_buffer().await;
if total_download_size > download_buffer.len() {
return reply_fail!(resp, "Download size is too big");
} else if total_download_size == 0 {
return reply_fail!(resp, "Zero download size");
}
// Starts the download
let download_buffer = &mut download_buffer[..total_download_size];
// `total_download_size` is parsed from `next_arg_u64` and thus should fit into u64.
resp.send_data_message(u64::try_from(total_download_size).unwrap()).await?;
let mut downloaded = 0;
while downloaded < total_download_size {
let (_, remains) = &mut download_buffer.split_at_mut(downloaded);
match resp.transport.receive_packet(remains).await? {
0 => yield_now().await,
v => match downloaded.checked_add(v) {
Some(v) if v > total_download_size => {
return reply_fail!(resp, "More data received then expected");
}
Some(v) => downloaded = v,
_ => return Err(Error::Other(Some("Invalid read size from transport"))),
},
};
}
match fb_impl.download_complete(downloaded, &mut resp).await {
Ok(()) => reply_okay!(resp, ""),
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
/// Helper for handling "fastboot flash ...".
async fn flash(
cmd: &str,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let flash_res =
match cmd.strip_prefix("flash:").ok_or::<CommandError>("Missing partition".into()) {
Ok(part) => fb_impl.flash(part, &mut resp).await,
Err(e) => Err(e),
};
match flash_res {
Err(e) => reply_fail!(resp, "{}", e.to_str()),
_ => reply_okay!(resp, ""),
}
}
/// Helper for handling "fastboot erase ...".
async fn erase(
cmd: &str,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let flash_res =
match cmd.strip_prefix("erase:").ok_or::<CommandError>("Missing partition".into()) {
Ok(part) => fb_impl.erase(part, &mut resp).await,
Err(e) => Err(e),
};
match flash_res {
Err(e) => reply_fail!(resp, "{}", e.to_str()),
_ => reply_okay!(resp, ""),
}
}
/// Helper for handling "fastboot get_staged ...".
async fn upload(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let upload_res = fb_impl.upload(&mut resp).await;
match resp.remaining_upload > 0 {
true => return Err(Error::InvalidInput),
_ => match upload_res {
Err(e) => reply_fail!(resp, "{}", e.to_str()),
_ => reply_okay!(resp, ""),
},
}
}
/// Helper for handling "fastboot fetch ...".
async fn fetch(
cmd: &str,
args: Split<'_, char>,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let fetch_res = async {
let cmd = cmd.strip_prefix("fetch:").ok_or::<CommandError>("Missing arguments".into())?;
if args.clone().count() < 3 {
return Err("Not enough argments".into());
}
// Parses backward. Parses size, offset first and treats the remaining string as
// partition name. This allows ":" in partition name.
let mut rev = args.clone().rev();
let sz = next_arg(&mut rev).ok_or("Missing size")?;
let off = next_arg(&mut rev).ok_or("Invalid offset")?;
let part = &cmd[..cmd.len() - (off.len() + sz.len() + 2)];
fb_impl.fetch(part, hex_to_u64(off)?, hex_to_u64(sz)?, &mut resp).await
}
.await;
match resp.remaining_upload > 0 {
true => return Err(Error::InvalidInput),
_ => match fetch_res {
Err(e) => reply_fail!(resp, "{}", e.to_str()),
_ => reply_okay!(resp, ""),
},
}
}
// Handles `fastboot reboot*`
async fn reboot(
mode: RebootMode,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let e = fb_impl.reboot(mode, &mut resp).await;
reply_fail!(resp, "{}", e.to_str())
}
// Handles `fastboot boot`
async fn boot(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let boot_res = async { fb_impl.boot(&mut resp).await }.await;
match boot_res {
Err(e) => reply_fail!(resp, "{}", e.to_str()),
_ => reply_okay!(resp, "boot_command"),
}
}
// Handles `fastboot continue`
async fn r#continue(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
match fb_impl.r#continue(&mut resp).await {
Ok(_) => reply_okay!(resp, ""),
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
// Handles `fastboot set_active`
async fn set_active(
mut args: Split<'_, char>,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let res = async {
let slot = next_arg(&mut args).ok_or("Missing slot")?;
fb_impl.set_active(slot, &mut resp).await
};
match res.await {
Ok(_) => reply_okay!(resp, ""),
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
/// Helper for handling "fastboot oem ...".
async fn oem(
cmd: &str,
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
let mut resp = Responder::new(transport);
let mut oem_out = [0u8; MAX_RESPONSE_SIZE - 4];
let oem_res = fb_impl.oem(cmd, &mut resp, &mut oem_out[..]).await;
match oem_res {
Ok(msg) => match from_utf8(msg) {
Ok(s) => reply_okay!(resp, "{}", s),
Err(e) => reply_fail!(resp, "Invalid return string {}", e),
},
Err(e) => reply_fail!(resp, "{}", e.to_str()),
}
}
/// Process the next Fastboot command from the transport.
///
/// # Returns
///
/// * Returns Ok(is_continue) on success where `is_continue` is true if command is
/// `fastboot continue`.
/// * Returns Err() on errors.
pub async fn process_next_command(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<bool> {
let mut packet = [0u8; MAX_COMMAND_SIZE + 1];
let cmd_size = match transport.receive_packet(&mut packet[..MAX_COMMAND_SIZE]).await? {
0 => return Ok(false),
v => v,
};
let Ok(cmd_str) = from_utf8(&packet[..cmd_size]) else {
transport.send_packet(fastboot_fail!(packet, "Invalid Command")).await?;
return Ok(false);
};
let mut args = cmd_str.split(':');
let Some(cmd) = args.next() else {
return transport.send_packet(fastboot_fail!(packet, "No command")).await.map(|_| false);
};
match cmd {
"boot" => {
boot(transport, fb_impl).await?;
return Ok(true);
}
"continue" => {
r#continue(transport, fb_impl).await?;
return Ok(true);
}
"download" => download(args, transport, fb_impl).await,
"erase" => erase(cmd_str, transport, fb_impl).await,
"fetch" => fetch(cmd_str, args, transport, fb_impl).await,
"flash" => flash(cmd_str, transport, fb_impl).await,
"getvar" => get_var(&mut packet[..], transport, fb_impl).await,
"reboot" => reboot(RebootMode::Normal, transport, fb_impl).await,
"reboot-bootloader" => reboot(RebootMode::Bootloader, transport, fb_impl).await,
"reboot-fastboot" => reboot(RebootMode::Fastboot, transport, fb_impl).await,
"reboot-recovery" => reboot(RebootMode::Recovery, transport, fb_impl).await,
"set_active" => set_active(args, transport, fb_impl).await,
"upload" => upload(transport, fb_impl).await,
_ if cmd_str.starts_with("oem ") => oem(&cmd_str[4..], transport, fb_impl).await,
_ => transport.send_packet(fastboot_fail!(packet, "Command not found")).await,
}?;
Ok(false)
}
/// Keeps polling and processing fastboot commands from the transport.
///
/// # Returns
///
/// * Returns Ok(()) if "fastboot continue" is received.
/// * Returns Err() on errors.
pub async fn run(
transport: &mut impl Transport,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
while !process_next_command(transport, fb_impl).await? {}
Ok(())
}
/// Runs a fastboot over TCP session.
///
/// The method performs fastboot over TCP handshake and then call `run(...)`.
///
/// Returns Ok(()) if "fastboot continue" is received.
pub async fn run_tcp_session(
tcp_stream: &mut impl TcpStream,
fb_impl: &mut impl FastbootImplementation,
) -> Result<()> {
run(&mut TcpTransport::new_and_handshake(tcp_stream)?, fb_impl).await
}
/// A helper to convert a hex string into u64.
pub(crate) fn hex_to_u64(s: &str) -> CommandResult<u64> {
Ok(u64::from_str_radix(s.strip_prefix("0x").unwrap_or(s), 16)?)
}
/// A helper to check and fetch the next non-empty argument.
///
/// # Args
///
/// args: A string iterator.
pub fn next_arg<'a, T: Iterator<Item = &'a str>>(args: &mut T) -> Option<&'a str> {
args.next().filter(|v| *v != "")
}
/// A helper to check and fetch the next argument as a u64 hex string.
///
/// # Args
///
/// args: A string iterator.
///
///
/// # Returns
///
/// * Returns Ok(Some(v)) is next argument is available and a valid u64 hex.
/// * Returns Ok(None) is next argument is not available
/// * Returns Err() if next argument is present but not a valid u64 hex.
pub fn next_arg_u64<'a, T: Iterator<Item = &'a str>>(args: &mut T) -> CommandResult<Option<u64>> {
match next_arg(args) {
Some(v) => Ok(Some(hex_to_u64(v)?)),
_ => Ok(None),
}
}
#[cfg(test)]
mod test {
use super::*;
use core::cmp::min;
use std::collections::{BTreeMap, VecDeque};
#[derive(Default)]
struct FastbootTest {
// A mapping from (variable name, argument) to variable value.
vars: BTreeMap<(&'static str, &'static [&'static str]), &'static str>,
// The partition arg from Fastboot flash command
flash_partition: String,
// The partition arg from Fastboot erase command
erase_partition: String,
// Upload size, batches of data to upload,
upload_config: (u64, Vec<Vec<u8>>),
// A map from partition name to (upload size override, partition data)
fetch_data: BTreeMap<&'static str, (u64, Vec<u8>)>,
// result string, INFO strings.
oem_output: (String, Vec<String>),
oem_command: String,
download_buffer: Vec<u8>,
downloaded_size: usize,
reboot_mode: Option<RebootMode>,
active_slot: Option<String>,
}
impl FastbootImplementation for FastbootTest {
async fn get_var(
&mut self,
var: &CStr,
args: impl Iterator<Item = &'_ CStr> + Clone,
out: &mut [u8],
_: impl InfoSender,
) -> CommandResult<usize> {
let args = args.map(|v| v.to_str().unwrap()).collect::<Vec<_>>();
match self.vars.get(&(var.to_str()?, &args[..])) {
Some(v) => {
out[..v.len()].clone_from_slice(v.as_bytes());
Ok(v.len())
}
_ => Err("Not Found".into()),
}
}
async fn get_var_all(&mut self, mut responder: impl VarInfoSender) -> CommandResult<()> {
for ((var, config), value) in &self.vars {
responder.send_var_info(var, config.iter().copied(), value).await?;
}
Ok(())
}
async fn get_download_buffer(&mut self) -> &mut [u8] {
self.download_buffer.as_mut_slice()
}
async fn download_complete(
&mut self,
download_size: usize,
_: impl InfoSender,
) -> CommandResult<()> {
self.downloaded_size = download_size;
Ok(())
}
async fn flash(&mut self, part: &str, _: impl InfoSender) -> CommandResult<()> {
self.flash_partition = part.into();
Ok(())
}
async fn erase(&mut self, part: &str, _: impl InfoSender) -> CommandResult<()> {
self.erase_partition = part.into();
Ok(())
}
async fn upload(&mut self, responder: impl UploadBuilder) -> CommandResult<()> {
let (size, batches) = &self.upload_config;
let mut uploader = responder.initiate_upload(*size).await?;
for ele in batches {
uploader.upload(&ele[..]).await?;
}
Ok(())
}
async fn fetch(
&mut self,
part: &str,
offset: u64,
size: u64,
responder: impl UploadBuilder + InfoSender,
) -> CommandResult<()> {
let (size_override, data) = self.fetch_data.get(part).ok_or("Not Found")?;
let mut uploader = responder.initiate_upload(*size_override).await?;
Ok(uploader
.upload(&data[offset.try_into().unwrap()..][..size.try_into().unwrap()])
.await?)
}
async fn boot(&mut self, mut responder: impl InfoSender + OkaySender) -> CommandResult<()> {
Ok(responder.send_info("Boot to boot.img...").await?)
}
async fn reboot(
&mut self,
mode: RebootMode,
responder: impl InfoSender + OkaySender,
) -> CommandError {
responder.send_okay("").await.unwrap();
self.reboot_mode = Some(mode);
"reboot-return".into()
}
async fn r#continue(&mut self, mut responder: impl InfoSender) -> CommandResult<()> {
Ok(responder.send_info("Continuing to boot...").await?)
}
async fn set_active(&mut self, slot: &str, _: impl InfoSender) -> CommandResult<()> {
self.active_slot = Some(slot.into());
Ok(())
}
async fn oem<'b>(
&mut self,
cmd: &str,
mut responder: impl InfoSender,
res: &'b mut [u8],
) -> CommandResult<&'b [u8]> {
let (res_str, infos) = &mut self.oem_output;
self.oem_command = cmd.into();
for ele in infos {
responder.send_info(ele.as_str()).await?;
}
Ok(snprintf!(res, "{}", *res_str).as_bytes())
}
}
struct TestTransport {
in_queue: VecDeque<Vec<u8>>,
out_queue: VecDeque<Vec<u8>>,
}
impl TestTransport {
fn new() -> Self {
Self { in_queue: VecDeque::new(), out_queue: VecDeque::new() }
}
fn add_input(&mut self, packet: &[u8]) {
self.in_queue.push_back(packet.into());
}
}
impl Transport for TestTransport {
async fn receive_packet(&mut self, out: &mut [u8]) -> Result<usize> {
match self.in_queue.pop_front() {
Some(v) => {
let size = min(out.len(), v.len());
out[..size].clone_from_slice(&v[..size]);
// Returns the input length so that we can test bogus download size.
Ok(v.len())
}
_ => Err(Error::Other(Some("No more data"))),
}
}
async fn send_packet(&mut self, packet: &[u8]) -> Result<()> {
self.out_queue.push_back(packet.into());
Ok(())
}
}
#[derive(Default)]
struct TestTcpStream {
in_queue: VecDeque<u8>,
out_queue: VecDeque<u8>,
}
impl TestTcpStream {
/// Adds bytes to input stream.
fn add_input(&mut self, data: &[u8]) {
data.iter().for_each(|v| self.in_queue.push_back(*v));
}
/// Adds a length pre-fixed bytes stream.
fn add_length_prefixed_input(&mut self, data: &[u8]) {
self.add_input(&(data.len() as u64).to_be_bytes());
self.add_input(data);
}
}
impl TcpStream for TestTcpStream {
async fn read_exact(&mut self, out: &mut [u8]) -> Result<()> {
for ele in out {
*ele = self.in_queue.pop_front().ok_or(Error::OperationProhibited)?;
}
Ok(())
}
async fn write_exact(&mut self, data: &[u8]) -> Result<()> {
data.iter().for_each(|v| self.out_queue.push_back(*v));
Ok(())
}
}
#[test]
fn test_boot() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"boot");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"INFOBoot to boot.img...".into(),
b"OKAYboot_command".into()
])
);
}
#[test]
fn test_non_exist_command() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"non_exist");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"FAILCommand not found"]);
}
#[test]
fn test_non_ascii_command_string() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"\xff\xff\xff");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"FAILInvalid Command"]);
}
#[test]
fn test_get_var_max_download_size() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"getvar:max-download-size");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"OKAY0x400"]);
}
#[test]
fn test_get_var() {
let mut fastboot_impl: FastbootTest = Default::default();
let vars: [((&str, &[&str]), &str); 4] = [
(("var_0", &[]), "val_0"),
(("var_1", &["a", "b"]), "val_1_a_b"),
(("var_1", &["c", "d"]), "val_1_c_d"),
(("var_2", &["e", "f"]), "val_2_e_f"),
];
fastboot_impl.vars = BTreeMap::from(vars);
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"getvar:var_0");
transport.add_input(b"getvar:var_1:a:b");
transport.add_input(b"getvar:var_1:c:d");
transport.add_input(b"getvar:var_1"); // Not Found
transport.add_input(b"getvar:var_2:e:f");
transport.add_input(b"getvar:var_3"); // Not Found
transport.add_input(b"getvar"); // Not Found
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"OKAYval_0".into(),
b"OKAYval_1_a_b".into(),
b"OKAYval_1_c_d".into(),
b"FAILNot Found".into(),
b"OKAYval_2_e_f".into(),
b"FAILNot Found".into(),
b"FAILMissing variable".into(),
])
);
}
#[test]
fn test_get_var_all() {
let mut fastboot_impl: FastbootTest = Default::default();
let vars: [((&str, &[&str]), &str); 4] = [
(("var_0", &[]), "val_0"),
(("var_1", &["a", "b"]), "val_1_a_b"),
(("var_1", &["c", "d"]), "val_1_c_d"),
(("var_2", &["e", "f"]), "val_2_e_f"),
];
fastboot_impl.vars = BTreeMap::from(vars);
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"getvar:all");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"INFOmax-download-size: 0x400".into(),
b"INFOvar_0: val_0".into(),
b"INFOvar_1:a:b: val_1_a_b".into(),
b"INFOvar_1:c:d: val_1_c_d".into(),
b"INFOvar_2:e:f: val_2_e_f".into(),
b"OKAY".into(),
])
);
}
#[test]
fn test_download() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let download_content: Vec<u8> =
(0..fastboot_impl.download_buffer.len()).into_iter().map(|v| v as u8).collect();
let mut transport = TestTransport::new();
// Splits download into two batches.
let (first, second) = download_content.as_slice().split_at(download_content.len() / 2);
transport.add_input(format!("download:{:#x}", download_content.len()).as_bytes());
transport.add_input(first);
transport.add_input(second);
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([b"DATA00000400".into(), b"OKAY".into(),])
);
assert_eq!(fastboot_impl.downloaded_size, download_content.len());
assert_eq!(fastboot_impl.download_buffer, download_content);
}
#[test]
fn test_download_not_enough_args() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"download");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"FAILNot enough argument"]);
}
#[test]
fn test_download_invalid_hex_string() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"download:hhh");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue.len(), 1);
assert!(transport.out_queue[0].starts_with(b"FAIL"));
}
fn test_download_size(download_buffer_size: usize, download_size: usize, msg: &str) {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; download_buffer_size];
let mut transport = TestTransport::new();
transport.add_input(format!("download:{:#x}", download_size).as_bytes());
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([msg.as_bytes().into()]));
}
#[test]
fn test_download_download_size_too_big() {
test_download_size(1024, 1025, "FAILDownload size is too big");
}
#[test]
fn test_download_zero_download_size() {
test_download_size(1024, 0, "FAILZero download size");
}
#[test]
fn test_download_more_than_expected() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let download_content: Vec<u8> = vec![0u8; fastboot_impl.download_buffer.len()];
let mut transport = TestTransport::new();
transport.add_input(format!("download:{:#x}", download_content.len() - 1).as_bytes());
transport.add_input(&download_content[..]);
// State should be reset to command state.
transport.add_input(b"getvar:max-download-size");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"DATA000003ff".into(),
b"FAILMore data received then expected".into(),
b"OKAY0x400".into(),
])
);
}
#[test]
fn test_oem_cmd() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"oem oem-command");
fastboot_impl.oem_output =
("oem-return".into(), vec!["oem-info-1".into(), "oem-info-2".into()]);
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(fastboot_impl.oem_command, "oem-command");
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"INFOoem-info-1".into(),
b"INFOoem-info-2".into(),
b"OKAYoem-return".into(),
])
);
}
#[test]
fn test_flash() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"flash:boot_a:0::");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(fastboot_impl.flash_partition, "boot_a:0::");
assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"OKAY".into()]));
}
#[test]
fn test_flash_missing_partition() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"flash");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"FAILMissing partition"]);
}
#[test]
fn test_erase() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"erase:boot_a:0::");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(fastboot_impl.erase_partition, "boot_a:0::");
assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"OKAY".into()]));
}
#[test]
fn test_erase_missing_partition() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"erase");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(transport.out_queue, [b"FAILMissing partition"]);
}
#[test]
fn test_upload() {
let mut fastboot_impl: FastbootTest = Default::default();
let upload_content: Vec<u8> = (0..1024).into_iter().map(|v| v as u8).collect();
let mut transport = TestTransport::new();
transport.add_input(b"upload");
fastboot_impl.upload_config = (
upload_content.len().try_into().unwrap(),
vec![
upload_content[..upload_content.len() / 2].to_vec(),
upload_content[upload_content.len() / 2..].to_vec(),
],
);
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"DATA00000400".into(),
upload_content[..upload_content.len() / 2].to_vec(),
upload_content[upload_content.len() / 2..].to_vec(),
b"OKAY".into(),
])
);
}
#[test]
fn test_upload_not_enough_data() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"upload");
fastboot_impl.upload_config = (0x400, vec![vec![0u8; 0x400 - 1]]);
assert!(block_on(run(&mut transport, &mut fastboot_impl)).is_err());
}
#[test]
fn test_upload_more_data() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"upload");
fastboot_impl.upload_config = (0x400, vec![vec![0u8; 0x400 + 1]]);
assert!(block_on(run(&mut transport, &mut fastboot_impl)).is_err());
}
#[test]
fn test_fetch() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"fetch:boot_a:0:::200:400");
fastboot_impl
.fetch_data
.insert("boot_a:0::", (0x400, vec![vec![0u8; 0x200], vec![1u8; 0x400]].concat()));
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"DATA00000400".into(),
[1u8; 0x400].to_vec(),
b"OKAY".into(),
])
);
}
#[test]
fn test_fetch_not_enough_data() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"fetch:boot_a:0:::200:400");
fastboot_impl
.fetch_data
.insert("boot_a:0::", (0x400 - 1, vec![vec![0u8; 0x200], vec![1u8; 0x400]].concat()));
assert!(block_on(process_next_command(&mut transport, &mut fastboot_impl)).is_err());
}
#[test]
fn test_fetch_more_data() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"fetch:boot_a:0:::200:400");
fastboot_impl
.fetch_data
.insert("boot_a:0::", (0x400 + 1, vec![vec![0u8; 0x200], vec![1u8; 0x400]].concat()));
assert!(block_on(process_next_command(&mut transport, &mut fastboot_impl)).is_err());
}
#[test]
fn test_fetch_invalid_args() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 2048];
let mut transport = TestTransport::new();
transport.add_input(b"fetch");
transport.add_input(b"fetch:");
transport.add_input(b"fetch:boot_a");
transport.add_input(b"fetch:boot_a:200");
transport.add_input(b"fetch:boot_a::400");
transport.add_input(b"fetch:boot_a::");
transport.add_input(b"fetch:boot_a:xxx:400");
transport.add_input(b"fetch:boot_a:200:xxx");
let _ = block_on(run(&mut transport, &mut fastboot_impl));
assert!(transport.out_queue.iter().all(|v| v.starts_with(b"FAIL")));
}
#[test]
fn test_fastboot_tcp() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let download_content: Vec<u8> =
(0..fastboot_impl.download_buffer.len()).into_iter().map(|v| v as u8).collect();
let mut tcp_stream: TestTcpStream = Default::default();
tcp_stream.add_input(TCP_HANDSHAKE_MESSAGE);
// Add two commands and verify both are executed.
tcp_stream.add_length_prefixed_input(b"getvar:max-download-size");
tcp_stream.add_length_prefixed_input(
format!("download:{:#x}", download_content.len()).as_bytes(),
);
tcp_stream.add_length_prefixed_input(&download_content[..]);
let _ = block_on(run_tcp_session(&mut tcp_stream, &mut fastboot_impl));
let expected: &[&[u8]] = &[
b"FB01",
b"\x00\x00\x00\x00\x00\x00\x00\x09OKAY0x400",
b"\x00\x00\x00\x00\x00\x00\x00\x0cDATA00000400",
b"\x00\x00\x00\x00\x00\x00\x00\x04OKAY",
];
assert_eq!(tcp_stream.out_queue, VecDeque::from(expected.concat()));
assert_eq!(fastboot_impl.download_buffer, download_content);
}
#[test]
fn test_fastboot_tcp_invalid_handshake() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut tcp_stream: TestTcpStream = Default::default();
tcp_stream.add_input(b"ABCD");
assert_eq!(
block_on(run_tcp_session(&mut tcp_stream, &mut fastboot_impl)).unwrap_err(),
Error::InvalidHandshake
);
}
#[test]
fn test_fastboot_tcp_packet_size_exceeds_maximum() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut tcp_stream: TestTcpStream = Default::default();
tcp_stream.add_input(TCP_HANDSHAKE_MESSAGE);
tcp_stream.add_input(&(MAX_COMMAND_SIZE + 1).to_be_bytes());
assert_eq!(
block_on(run_tcp_session(&mut tcp_stream, &mut fastboot_impl)).unwrap_err(),
Error::InvalidInput
);
}
#[test]
fn test_reboot() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"reboot");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(fastboot_impl.reboot_mode, Some(RebootMode::Normal));
assert_eq!(transport.out_queue[0], b"OKAY");
// Failure is expected here because test reboot implementation always returns, which
// automatically generates a fastboot failure packet.
assert!(transport.out_queue[1].starts_with(b"FAIL"));
}
#[test]
fn test_reboot_bootloader() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"reboot-bootloader");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(fastboot_impl.reboot_mode, Some(RebootMode::Bootloader));
assert_eq!(transport.out_queue[0], b"OKAY");
// Failure is expected here because test reboot implementation always returns, which
// automatically generates a fastboot failure packet.
assert!(transport.out_queue[1].starts_with(b"FAIL"));
}
#[test]
fn test_reboot_fastboot() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"reboot-fastboot");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(fastboot_impl.reboot_mode, Some(RebootMode::Fastboot));
assert_eq!(transport.out_queue[0], b"OKAY");
// Failure is expected here because test reboot implementation always returns, which
// automatically generates a fastboot failure packet.
assert!(transport.out_queue[1].starts_with(b"FAIL"));
}
#[test]
fn test_reboot_recovery() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut transport = TestTransport::new();
transport.add_input(b"reboot-recovery");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(fastboot_impl.reboot_mode, Some(RebootMode::Recovery));
assert_eq!(transport.out_queue[0], b"OKAY");
// Failure is expected here because test reboot implementation always returns, which
// automatically generates a fastboot failure packet.
assert!(transport.out_queue[1].starts_with(b"FAIL"));
}
#[test]
fn test_continue() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"getvar:max-download-size");
transport.add_input(b"continue");
transport.add_input(b"getvar:max-download-size");
block_on(run(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(
transport.out_queue,
VecDeque::<Vec<u8>>::from([
b"OKAY0x400".into(),
b"INFOContinuing to boot...".into(),
b"OKAY".into()
])
);
}
#[test]
fn test_continue_run_tcp() {
let mut fastboot_impl: FastbootTest = Default::default();
let mut tcp_stream: TestTcpStream = Default::default();
tcp_stream.add_input(TCP_HANDSHAKE_MESSAGE);
tcp_stream.add_length_prefixed_input(b"continue");
block_on(run_tcp_session(&mut tcp_stream, &mut fastboot_impl)).unwrap();
}
#[test]
fn test_set_active() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"set_active:a");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"OKAY".into()]));
assert_eq!(fastboot_impl.active_slot, Some("a".into()));
}
#[test]
fn test_set_active_missing_slot() {
let mut fastboot_impl: FastbootTest = Default::default();
fastboot_impl.download_buffer = vec![0u8; 1024];
let mut transport = TestTransport::new();
transport.add_input(b"set_active");
block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"FAILMissing slot".into()]));
}
}