blob: 13bf61c55c1b100bd55d535bbc148729aac310a0 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::{
file::FileResolver,
manifest::{flash_partition, reboot_bootloader, stage_file, verify_variable_value, Flash},
},
anyhow::{anyhow, Result},
async_trait::async_trait,
errors::{ffx_bail, ffx_error},
ffx_flash_args::{FlashCommand, OemFile},
fidl_fuchsia_developer_bridge::FastbootProxy,
serde::{Deserialize, Serialize},
std::io::Write,
};
const REBOOT_ERR: &str = "Failed to reboot your device. \
Power cycle the device manually and try again.";
pub(crate) const MISSING_PRODUCT: &str = "Manifest does not contain product";
const UNLOCK_ERR: &str = "The product requires the target is unlocked. \
Please unlock target and try again.";
const LOCKED_VAR: &str = "vx-locked";
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct Product {
pub(crate) name: String,
pub(crate) bootloader_partitions: Vec<Partition>,
pub(crate) partitions: Vec<Partition>,
pub(crate) oem_files: Vec<OemFile>,
#[serde(default)]
pub(crate) requires_unlock: bool,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct Partition(
String,
String,
#[serde(default)] Option<String>,
#[serde(default)] Option<String>,
);
impl Partition {
pub(crate) fn new(
name: String,
file: String,
variable: Option<String>,
variable_value: Option<String>,
) -> Self {
Partition(name, file, variable, variable_value)
}
pub(crate) fn name(&self) -> &str {
self.0.as_str()
}
pub(crate) fn file(&self) -> &str {
self.1.as_str()
}
pub(crate) fn variable(&self) -> Option<&str> {
self.2.as_ref().map(|s| s.as_str())
}
pub(crate) fn variable_value(&self) -> Option<&str> {
self.3.as_ref().map(|s| s.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct FlashManifest(pub(crate) Vec<Product>);
#[async_trait(?Send)]
impl Flash for FlashManifest {
async fn flash<W, F>(
&self,
writer: &mut W,
file_resolver: &mut F,
fastboot_proxy: FastbootProxy,
cmd: FlashCommand,
) -> Result<()>
where
W: Write,
F: FileResolver + Sync,
{
let product = match self.0.iter().find(|product| product.name == cmd.product) {
Some(res) => res,
None => ffx_bail!("{} {}", MISSING_PRODUCT, cmd.product),
};
if product.requires_unlock
&& !verify_variable_value(LOCKED_VAR, "no", &fastboot_proxy).await?
{
ffx_bail!("{}", UNLOCK_ERR);
}
flash_partitions(writer, file_resolver, &product.bootloader_partitions, &fastboot_proxy)
.await?;
if product.bootloader_partitions.len() > 0 && !cmd.no_bootloader_reboot {
reboot_bootloader(writer, &fastboot_proxy)
.await
.map_err(|_| ffx_error!("{}", REBOOT_ERR))?;
}
stage_oem_files(writer, file_resolver, false, &cmd.oem_stage, &fastboot_proxy).await?;
flash_partitions(writer, file_resolver, &product.partitions, &fastboot_proxy).await?;
stage_oem_files(writer, file_resolver, true, &product.oem_files, &fastboot_proxy).await?;
if fastboot_proxy.erase("misc").await?.is_err() {
log::debug!("Could not erase misc partition");
}
fastboot_proxy.set_active("a").await?.map_err(|_| anyhow!("Could not set active slot"))?;
fastboot_proxy.continue_boot().await?.map_err(|_| anyhow!("Could not reboot device"))?;
writeln!(writer, "Continuing to boot - this could take awhile")?;
Ok(())
}
}
pub(crate) async fn stage_oem_files<W: Write, F: FileResolver + Sync>(
writer: &mut W,
file_resolver: &mut F,
resolve: bool,
oem_files: &Vec<OemFile>,
fastboot_proxy: &FastbootProxy,
) -> Result<()> {
for oem_file in oem_files {
stage_file(writer, file_resolver, resolve, oem_file.file(), &fastboot_proxy).await?;
writeln!(writer, "Sending command \"{}\"", oem_file.command())?;
fastboot_proxy.oem(oem_file.command()).await?.map_err(|_| {
anyhow!("There was an error sending oem command \"{}\"", oem_file.command())
})?;
}
Ok(())
}
pub(crate) async fn flash_partitions<W: Write, F: FileResolver + Sync>(
writer: &mut W,
file_resolver: &mut F,
partitions: &Vec<Partition>,
fastboot_proxy: &FastbootProxy,
) -> Result<()> {
for partition in partitions {
match (partition.variable(), partition.variable_value()) {
(Some(var), Some(value)) => {
if verify_variable_value(var, value, fastboot_proxy).await? {
flash_partition(
writer,
file_resolver,
partition.name(),
partition.file(),
fastboot_proxy,
)
.await?;
}
}
_ => {
flash_partition(
writer,
file_resolver,
partition.name(),
partition.file(),
fastboot_proxy,
)
.await?
}
}
}
Ok(())
}
////////////////////////////////////////////////////////////////////////////////
// tests
#[cfg(test)]
mod test {
use super::*;
use crate::test::{setup, TestResolver};
use regex::Regex;
use serde_json::from_str;
use std::path::PathBuf;
use tempfile::NamedTempFile;
const SIMPLE_MANIFEST: &'static str = r#"[
{
"name": "fuchsia",
"bootloader_partitions": [],
"partitions": [
["test1", "path1"],
["test2", "path2"],
["test3", "path3"],
["test4", "path4"],
["test5", "path5"]
],
"oem_files": []
}
]"#;
const MANIFEST: &'static str = r#"[
{
"name": "zedboot",
"bootloader_partitions": [
["test1", "path1"],
["test2", "path2"]
],
"partitions": [
["test1", "path1"],
["test2", "path2"],
["test3", "path3"],
["test4", "path4"],
["test5", "path5"]
],
"oem_files": [
["test1", "path1"],
["test2", "path2"]
]
},
{
"name": "fuchsia",
"bootloader_partitions": [],
"partitions": [
["test10", "path10"],
["test20", "path20"],
["test30", "path30"]
],
"oem_files": []
}
]"#;
const CONDITIONAL_MANIFEST: &'static str = r#"[
{
"name": "zedboot",
"bootloader_partitions": [
["btest1", "bpath1", "var1", "value1"],
["btest2", "bpath2", "var2", "value2"],
["btest3", "bpath3", "var3", "value3"]
],
"partitions": [],
"oem_files": []
}
]"#;
const LOCKED_MANIFEST: &'static str = r#"[
{
"name": "zedboot",
"bootloader_partitions": [
["btest1", "bpath1", "var1", "value1"]
],
"partitions": [],
"oem_files": [],
"requires_unlock": true
}
]"#;
#[fuchsia_async::run_singlethreaded(test)]
async fn test_deserializing_should_work() -> Result<()> {
let v: FlashManifest = from_str(MANIFEST)?;
let zedboot: &Product = &v.0[0];
assert_eq!("zedboot", zedboot.name);
assert_eq!(2, zedboot.bootloader_partitions.len());
let bootloader_expected = [["test1", "path1"], ["test2", "path2"]];
for x in 0..bootloader_expected.len() {
assert_eq!(zedboot.bootloader_partitions[x].name(), bootloader_expected[x][0]);
assert_eq!(zedboot.bootloader_partitions[x].file(), bootloader_expected[x][1]);
}
assert_eq!(5, zedboot.partitions.len());
let expected = [
["test1", "path1"],
["test2", "path2"],
["test3", "path3"],
["test4", "path4"],
["test5", "path5"],
];
for x in 0..expected.len() {
assert_eq!(zedboot.partitions[x].name(), expected[x][0]);
assert_eq!(zedboot.partitions[x].file(), expected[x][1]);
}
assert_eq!(2, zedboot.oem_files.len());
let oem_files_expected = [["test1", "path1"], ["test2", "path2"]];
for x in 0..oem_files_expected.len() {
assert_eq!(zedboot.oem_files[x].command(), oem_files_expected[x][0]);
assert_eq!(zedboot.oem_files[x].file(), oem_files_expected[x][1]);
}
let product: &Product = &v.0[1];
assert_eq!("fuchsia", product.name);
assert_eq!(0, product.bootloader_partitions.len());
assert_eq!(3, product.partitions.len());
let expected2 = [["test10", "path10"], ["test20", "path20"], ["test30", "path30"]];
for x in 0..expected2.len() {
assert_eq!(product.partitions[x].name(), expected2[x][0]);
assert_eq!(product.partitions[x].file(), expected2[x][1]);
}
assert_eq!(0, product.oem_files.len());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_should_fail_if_product_missing() -> Result<()> {
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let v: FlashManifest = from_str(MANIFEST)?;
let (_, proxy) = setup();
let mut writer = Vec::<u8>::new();
assert!(v
.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(tmp_file_name)),
product: "Unknown".to_string(),
..Default::default()
}
)
.await
.is_err());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_should_succeed_if_product_found() -> Result<()> {
let v: FlashManifest = from_str(MANIFEST)?;
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let (_, proxy) = setup();
let mut writer = Vec::<u8>::new();
v.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(tmp_file_name)),
product: "fuchsia".to_string(),
..Default::default()
},
)
.await?;
let output = String::from_utf8(writer).expect("utf-8 string");
for partition in &v.0[1].partitions {
let name_listing = Regex::new(&partition.name()).expect("test regex");
let path_listing = Regex::new(&partition.file()).expect("test regex");
assert_eq!(name_listing.find_iter(&output).count(), 1);
assert_eq!(path_listing.find_iter(&output).count(), 1);
}
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_oem_file_should_be_staged_from_command() -> Result<()> {
let v: FlashManifest = from_str(SIMPLE_MANIFEST)?;
let (state, proxy) = setup();
let test_oem_cmd = "test-oem-cmd";
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let test_staged_file = format!("{},{}", test_oem_cmd, tmp_file_name).parse::<OemFile>()?;
let manifest_file = NamedTempFile::new().expect("tmp access failed");
let manifest_file_name = manifest_file.path().to_string_lossy().to_string();
let mut writer = Vec::<u8>::new();
v.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(manifest_file_name)),
product: "fuchsia".to_string(),
oem_stage: vec![test_staged_file],
..Default::default()
},
)
.await?;
let state = state.lock().unwrap();
assert_eq!(1, state.staged_files.len());
assert_eq!(1, state.oem_commands.len());
assert_eq!(test_oem_cmd, state.oem_commands[0]);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_should_upload_conditional_partitions_that_match() -> Result<()> {
let v: FlashManifest = from_str(CONDITIONAL_MANIFEST)?;
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let (state, proxy) = setup();
{
let mut state = state.lock().unwrap();
state.variables.push("not_value3".to_string());
state.variables.push("value2".to_string());
state.variables.push("not_value1".to_string());
}
let mut writer = Vec::<u8>::new();
v.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(tmp_file_name)),
product: "zedboot".to_string(),
..Default::default()
},
)
.await?;
let output = String::from_utf8(writer).expect("utf-8 string");
for (i, partition) in v.0[0].bootloader_partitions.iter().enumerate() {
let name_listing = Regex::new(&partition.name()).expect("test regex");
let path_listing = Regex::new(&partition.file()).expect("test regex");
let expected = if i == 1 { 1 } else { 0 };
assert_eq!(name_listing.find_iter(&output).count(), expected);
assert_eq!(path_listing.find_iter(&output).count(), expected);
}
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_should_succeed_and_not_reboot_bootloader() -> Result<()> {
let v: FlashManifest = from_str(MANIFEST)?;
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let (state, proxy) = setup();
let mut writer = Vec::<u8>::new();
v.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(tmp_file_name)),
product: "fuchsia".to_string(),
no_bootloader_reboot: true,
..Default::default()
},
)
.await?;
let state = state.lock().unwrap();
assert_eq!(0, state.bootloader_reboots);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_should_not_flash_if_target_is_locked_and_product_requires_unlock() -> Result<()> {
let v: FlashManifest = from_str(LOCKED_MANIFEST)?;
let tmp_file = NamedTempFile::new().expect("tmp access failed");
let tmp_file_name = tmp_file.path().to_string_lossy().to_string();
let (state, proxy) = setup();
{
let mut state = state.lock().unwrap();
state.variables.push("vx-locked".to_string());
state.variables.push("yes".to_string());
}
let mut writer = Vec::<u8>::new();
let res = v
.flash(
&mut writer,
&mut TestResolver::new(),
proxy,
FlashCommand {
manifest: Some(PathBuf::from(tmp_file_name)),
product: "zedboot".to_string(),
..Default::default()
},
)
.await;
assert_eq!(true, res.is_err());
Ok(())
}
}