diff options
Diffstat (limited to 'util/flashrom_tester')
-rw-r--r-- | util/flashrom_tester/.cargo/config.toml | 2 | ||||
-rw-r--r-- | util/flashrom_tester/.gitignore | 2 | ||||
-rw-r--r-- | util/flashrom_tester/Cargo.toml | 39 | ||||
-rw-r--r-- | util/flashrom_tester/build.rs | 5 | ||||
-rw-r--r-- | util/flashrom_tester/flashrom/Cargo.toml | 12 | ||||
-rw-r--r-- | util/flashrom_tester/flashrom/src/cmd.rs | 590 | ||||
-rw-r--r-- | util/flashrom_tester/flashrom/src/flashromlib.rs | 184 | ||||
-rw-r--r-- | util/flashrom_tester/flashrom/src/lib.rs | 165 | ||||
-rwxr-xr-x | util/flashrom_tester/flashrom_remote.sh | 2 | ||||
-rw-r--r-- | util/flashrom_tester/src/cros_sysinfo.rs | 70 | ||||
-rw-r--r-- | util/flashrom_tester/src/lib.rs | 46 | ||||
-rw-r--r-- | util/flashrom_tester/src/logger.rs | 149 | ||||
-rw-r--r-- | util/flashrom_tester/src/main.rs | 211 | ||||
-rw-r--r-- | util/flashrom_tester/src/rand_util.rs | 80 | ||||
-rw-r--r-- | util/flashrom_tester/src/tester.rs | 567 | ||||
-rw-r--r-- | util/flashrom_tester/src/tests.rs | 398 | ||||
-rw-r--r-- | util/flashrom_tester/src/types.rs | 72 | ||||
-rw-r--r-- | util/flashrom_tester/src/utils.rs | 241 |
18 files changed, 2835 insertions, 0 deletions
diff --git a/util/flashrom_tester/.cargo/config.toml b/util/flashrom_tester/.cargo/config.toml new file mode 100644 index 00000000..8af59dd8 --- /dev/null +++ b/util/flashrom_tester/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +RUST_TEST_THREADS = "1" diff --git a/util/flashrom_tester/.gitignore b/util/flashrom_tester/.gitignore new file mode 100644 index 00000000..1e7caa9e --- /dev/null +++ b/util/flashrom_tester/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/util/flashrom_tester/Cargo.toml b/util/flashrom_tester/Cargo.toml new file mode 100644 index 00000000..e6ed9c04 --- /dev/null +++ b/util/flashrom_tester/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "flashrom_tester" +version = "1.6.0" +authors = ["Edward O'Callaghan <quasisec@chromium.org>", + "Peter Marheine <pmarheine@chromium.org>"] +description = "A tool to verify flashrom and flash chip behaviour." +license = "GPL-2.0-only" +edition = "2018" +build = "build.rs" + +[lib] +name = "flashrom_tester" + +[[bin]] +name = "flashrom_tester" +required-features = ["cli"] + +[dependencies] +atty = "0.2" +built = { version = "0.5", features = ["chrono"] } +chrono = { version = "0.4", optional = true } +clap = { version = "2.33", default-features = false, optional = true } +flashrom = { path = "flashrom/" } +libc = "0.2" +log = { version = "0.4", features = ["std"] } +rand = "0.6.4" +serde_json = "1" +sys-info = "0.9" + +[build-dependencies] +built = { version = "0.5", features = ["chrono"] } + +[dev-dependencies] +gag = "1" + +[features] +# Features required to build the CLI binary but not the library +cli = ["chrono", "clap"] +default = ["cli"] diff --git a/util/flashrom_tester/build.rs b/util/flashrom_tester/build.rs new file mode 100644 index 00000000..3800c173 --- /dev/null +++ b/util/flashrom_tester/build.rs @@ -0,0 +1,5 @@ +extern crate built; + +fn main() { + built::write_built_file().expect("Failed to acquire build-time information"); +} diff --git a/util/flashrom_tester/flashrom/Cargo.toml b/util/flashrom_tester/flashrom/Cargo.toml new file mode 100644 index 00000000..4d4fc2fe --- /dev/null +++ b/util/flashrom_tester/flashrom/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "flashrom" +version = "1.0.0" +authors = ["Edward O'Callaghan <quasisec@chromium.org>", + "Peter Marheine <pmarheine@chromium.org>"] +description = "Flashrom abstraction for the flashrom_tester tool." +license = "GPL-2.0-only" +edition = "2018" + +[dependencies] +log = "0.4" +libflashrom = { path = "../../../bindings/rust/libflashrom" } diff --git a/util/flashrom_tester/flashrom/src/cmd.rs b/util/flashrom_tester/flashrom/src/cmd.rs new file mode 100644 index 00000000..1f13a8ed --- /dev/null +++ b/util/flashrom_tester/flashrom/src/cmd.rs @@ -0,0 +1,590 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use crate::{FlashChip, FlashromError}; + +use std::{ + ffi::{OsStr, OsString}, + path::Path, + process::Command, +}; + +#[derive(Default)] +pub struct FlashromOpt<'a> { + pub wp_opt: WPOpt, + pub io_opt: Option<IOOpt<'a>>, + + pub flash_name: bool, // --flash-name + pub verbose: bool, // -V +} + +#[derive(Default)] +pub struct WPOpt { + pub range: Option<(i64, i64)>, // --wp-range x0 x1 + pub status: bool, // --wp-status + pub list: bool, // --wp-list + pub enable: bool, // --wp-enable + pub disable: bool, // --wp-disable +} + +pub enum OperationArgs<'a> { + /// The file is the whole chip. + EntireChip(&'a Path), + /// File is the size of the full chip, limited to a single named region. + /// + /// The required path is the file to use, and the optional path is a layout file + /// specifying how to locate regions (if unspecified, flashrom will attempt + /// to discover the layout itself). + FullFileRegion(&'a str, &'a Path, Option<&'a Path>), + /// File is the size of the single named region only. + /// + /// The required path is the file to use, and the optional path is a layout file + /// specifying how to locate regions (if unspecified, flashrom will attempt + /// to discover the layout itself). + RegionFileRegion(&'a str, &'a Path, Option<&'a Path>), // The file contains only the region +} + +pub enum IOOpt<'a> { + Read(OperationArgs<'a>), // -r <file> + Write(OperationArgs<'a>), // -w <file> + Verify(OperationArgs<'a>), // -v <file> + Erase, // -E +} + +#[derive(PartialEq, Eq, Debug)] +pub struct FlashromCmd { + pub path: String, + pub fc: FlashChip, +} + +/// Attempt to determine the Flash size given stdout from `flashrom --flash-size` +fn flashrom_extract_size(stdout: &str) -> Result<i64, FlashromError> { + // Search for the last line of output that contains only digits, assuming + // that's the actual size. flashrom sadly tends to write additional messages + // to stdout. + match stdout + .lines() + .filter(|line| line.chars().all(|c| c.is_ascii_digit())) + .last() + .map(str::parse::<i64>) + { + None => Err("Found no purely-numeric lines in flashrom output".into()), + Some(Err(e)) => { + Err(format!("Failed to parse flashrom size output as integer: {}", e).into()) + } + Some(Ok(sz)) => Ok(sz), + } +} + +impl FlashromCmd { + fn dispatch( + &self, + fropt: FlashromOpt, + debug_name: &str, + ) -> Result<(String, String), FlashromError> { + let params = flashrom_decode_opts(fropt); + flashrom_dispatch(self.path.as_str(), ¶ms, self.fc, debug_name) + } +} + +impl crate::Flashrom for FlashromCmd { + fn get_size(&self) -> Result<i64, FlashromError> { + let (stdout, _) = + flashrom_dispatch(self.path.as_str(), &["--flash-size"], self.fc, "get_size")?; + flashrom_extract_size(&stdout) + } + + fn name(&self) -> Result<(String, String), FlashromError> { + let opts = FlashromOpt { + flash_name: true, + ..Default::default() + }; + + let (stdout, _) = self.dispatch(opts, "name")?; + match extract_flash_name(&stdout) { + None => Err("Didn't find chip vendor/name in flashrom output".into()), + Some((vendor, name)) => Ok((vendor.into(), name.into())), + } + } + + fn write_from_file_region( + &self, + path: &Path, + region: &str, + layout: &Path, + ) -> Result<bool, FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Write(OperationArgs::FullFileRegion( + region, + path, + Some(layout), + ))), + ..Default::default() + }; + + self.dispatch(opts, "write_file_with_layout")?; + Ok(true) + } + + fn wp_range(&self, range: (i64, i64), en: bool) -> Result<bool, FlashromError> { + let opts = FlashromOpt { + wp_opt: WPOpt { + enable: en, + disable: !en, + range: Some(range), + ..Default::default() + }, + ..Default::default() + }; + + self.dispatch(opts, "wp_range")?; + Ok(true) + } + + fn wp_list(&self) -> Result<String, FlashromError> { + let opts = FlashromOpt { + wp_opt: WPOpt { + list: true, + ..Default::default() + }, + ..Default::default() + }; + + let (stdout, _) = self.dispatch(opts, "wp_list")?; + if stdout.is_empty() { + return Err( + "wp_list isn't supported on platforms using the Linux kernel SPI driver wp_list" + .into(), + ); + } + Ok(stdout) + } + + fn wp_status(&self, en: bool) -> Result<bool, FlashromError> { + let status = if en { "en" } else { "dis" }; + info!("See if chip write protect is {}abled", status); + + let opts = FlashromOpt { + wp_opt: WPOpt { + status: true, + ..Default::default() + }, + ..Default::default() + }; + + let (stdout, _) = self.dispatch(opts, "wp_status")?; + let s = std::format!("write protect is {}abled", status); + Ok(stdout.contains(&s)) + } + + fn wp_toggle(&self, en: bool) -> Result<bool, FlashromError> { + let range = if en { + let rom_sz: i64 = self.get_size()?; + (0, rom_sz) // (start, len) + } else { + (0, 0) + }; + self.wp_range(range, en)?; + let status = if en { "en" } else { "dis" }; + match self.wp_status(true) { + Ok(_ret) => { + info!("Successfully {}abled write-protect", status); + Ok(true) + } + Err(e) => Err(format!("Cannot {}able write-protect: {}", status, e).into()), + } + } + + fn read_into_file(&self, path: &Path) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Read(OperationArgs::EntireChip(path))), + ..Default::default() + }; + + self.dispatch(opts, "read_into_file")?; + Ok(()) + } + + fn read_region_into_file(&self, path: &Path, region: &str) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Read(OperationArgs::RegionFileRegion( + region, path, None, + ))), + ..Default::default() + }; + + self.dispatch(opts, "read_region_into_file")?; + Ok(()) + } + + fn write_from_file(&self, path: &Path) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Write(OperationArgs::EntireChip(path))), + ..Default::default() + }; + + self.dispatch(opts, "write_from_file")?; + Ok(()) + } + + fn verify_from_file(&self, path: &Path) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Verify(OperationArgs::EntireChip(path))), + ..Default::default() + }; + + self.dispatch(opts, "verify_from_file")?; + Ok(()) + } + + fn verify_region_from_file(&self, path: &Path, region: &str) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Verify(OperationArgs::RegionFileRegion( + region, path, None, + ))), + ..Default::default() + }; + + self.dispatch(opts, "verify_region_from_file")?; + Ok(()) + } + + fn erase(&self) -> Result<(), FlashromError> { + let opts = FlashromOpt { + io_opt: Some(IOOpt::Erase), + ..Default::default() + }; + + self.dispatch(opts, "erase")?; + Ok(()) + } + + fn can_control_hw_wp(&self) -> bool { + self.fc.can_control_hw_wp() + } +} + +fn flashrom_decode_opts(opts: FlashromOpt) -> Vec<OsString> { + let mut params = Vec::<OsString>::new(); + + // ------------ WARNING !!! ------------ + // each param must NOT contain spaces! + // ------------------------------------- + + // wp_opt + if opts.wp_opt.range.is_some() { + let (x0, x1) = opts.wp_opt.range.unwrap(); + params.push("--wp-range".into()); + params.push(hex_range_string(x0, x1).into()); + } + if opts.wp_opt.status { + params.push("--wp-status".into()); + } else if opts.wp_opt.list { + params.push("--wp-list".into()); + } else if opts.wp_opt.enable { + params.push("--wp-enable".into()); + } else if opts.wp_opt.disable { + params.push("--wp-disable".into()); + } + + // io_opt + fn add_operation_args(opts: OperationArgs, params: &mut Vec<OsString>) { + let (file, region, layout) = match opts { + OperationArgs::EntireChip(file) => (Some(file), None, None), + OperationArgs::FullFileRegion(region, file, layout) => { + (Some(file), Some(region.to_string()), layout) + } + OperationArgs::RegionFileRegion(region, file, layout) => ( + None, + Some(format!("{region}:{}", file.to_string_lossy())), + layout, + ), + }; + if let Some(file) = file { + params.push(file.into()) + } + if let Some(region) = region { + params.push("--include".into()); + params.push(region.into()) + } + if let Some(layout) = layout { + params.push("--layout".into()); + params.push(layout.into()) + } + } + if let Some(io) = opts.io_opt { + match io { + IOOpt::Read(args) => { + params.push("-r".into()); + add_operation_args(args, &mut params); + } + IOOpt::Write(args) => { + params.push("-w".into()); + add_operation_args(args, &mut params); + } + IOOpt::Verify(args) => { + params.push("-v".into()); + add_operation_args(args, &mut params); + } + IOOpt::Erase => params.push("-E".into()), + } + } + + // misc_opt + if opts.flash_name { + params.push("--flash-name".into()); + } + if opts.verbose { + params.push("-V".into()); + } + + params +} + +fn flashrom_dispatch<S: AsRef<OsStr>>( + path: &str, + params: &[S], + fc: FlashChip, + debug_name: &str, +) -> Result<(String, String), FlashromError> { + // from man page: + // ' -p, --programmer <name>[:parameter[,parameter[,parameter]]] ' + let mut args: Vec<&OsStr> = vec![OsStr::new("-p"), OsStr::new(FlashChip::to(fc))]; + args.extend(params.iter().map(S::as_ref)); + + info!("flashrom_dispatch() running: {} {:?}", path, args); + + let output = match Command::new(path).args(&args).output() { + Ok(x) => x, + Err(e) => return Err(format!("Failed to run flashrom: {}", e).into()), + }; + + let stdout = String::from_utf8_lossy(output.stdout.as_slice()); + let stderr = String::from_utf8_lossy(output.stderr.as_slice()); + debug!("{}()'stdout: {}.", debug_name, stdout); + debug!("{}()'stderr: {}.", debug_name, stderr); + + if !output.status.success() { + // There is two cases on failure; + // i. ) A bad exit code, + // ii.) A SIG killed us. + match output.status.code() { + Some(code) => { + return Err(format!("{}\nExited with error code: {}", stderr, code).into()); + } + None => return Err("Process terminated by a signal".into()), + } + } + + Ok((stdout.into(), stderr.into())) +} + +fn hex_range_string(s: i64, l: i64) -> String { + format!("{:#08X},{:#08X}", s, l) +} + +/// Get a flash vendor and name from the first matching line of flashrom output. +/// +/// The target line looks like 'vendor="foo" name="bar"', as output by flashrom --flash-name. +/// This is usually the last line of output. +fn extract_flash_name(stdout: &str) -> Option<(&str, &str)> { + for line in stdout.lines() { + if !line.starts_with("vendor=\"") { + continue; + } + + let tail = line.trim_start_matches("vendor=\""); + let mut split = tail.splitn(2, "\" name=\""); + let vendor = split.next(); + let name = split.next().map(|s| s.trim_end_matches('"')); + + match (vendor, name) { + (Some(v), Some(n)) => return Some((v, n)), + _ => continue, + } + } + None +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::flashrom_decode_opts; + use super::{FlashromOpt, IOOpt, WPOpt}; + + #[test] + fn decode_wp_opt() { + fn test_wp_opt(wpo: WPOpt, expected: &[&str]) { + assert_eq!( + flashrom_decode_opts(FlashromOpt { + wp_opt: wpo, + ..Default::default() + }), + expected + ); + } + + test_wp_opt(Default::default(), &[]); + test_wp_opt( + WPOpt { + range: Some((0, 1234)), + status: true, + ..Default::default() + }, + &["--wp-range", "0x000000,0x0004D2", "--wp-status"], + ); + test_wp_opt( + WPOpt { + list: true, + ..Default::default() + }, + &["--wp-list"], + ); + test_wp_opt( + WPOpt { + enable: true, + ..Default::default() + }, + &["--wp-enable"], + ); + test_wp_opt( + WPOpt { + disable: true, + ..Default::default() + }, + &["--wp-disable"], + ); + } + + #[test] + fn decode_io_opt() { + fn test_io_opt(opts: IOOpt, expected: &[&str]) { + assert_eq!( + flashrom_decode_opts(FlashromOpt { + io_opt: Some(opts), + ..Default::default() + }), + expected + ); + } + + test_io_opt( + IOOpt::Read(crate::cmd::OperationArgs::EntireChip(Path::new("foo.bin"))), + &["-r", "foo.bin"], + ); + test_io_opt( + IOOpt::Write(crate::cmd::OperationArgs::EntireChip(Path::new("bar.bin"))), + &["-w", "bar.bin"], + ); + test_io_opt( + IOOpt::Verify(crate::cmd::OperationArgs::EntireChip(Path::new("baz.bin"))), + &["-v", "baz.bin"], + ); + test_io_opt(IOOpt::Erase, &["-E"]); + test_io_opt( + IOOpt::Read(crate::cmd::OperationArgs::FullFileRegion( + "RO", + Path::new("foo.bin"), + Some(Path::new("baz.bin")), + )), + &["-r", "foo.bin", "--include", "RO", "--layout", "baz.bin"], + ); + + test_io_opt( + IOOpt::Read(crate::cmd::OperationArgs::RegionFileRegion( + "foo", + Path::new("bar.bin"), + None, + )), + &["-r", "--include", "foo:bar.bin"], + ) + } + + #[test] + fn decode_misc() { + //use Default::default; + + assert_eq!( + flashrom_decode_opts(FlashromOpt { + flash_name: true, + verbose: true, + ..Default::default() + }), + &["--flash-name", "-V"] + ); + } + + #[test] + fn flashrom_extract_size() { + use super::flashrom_extract_size; + + assert_eq!( + flashrom_extract_size( + "coreboot table found at 0x7cc13000.\n\ + Found chipset \"Intel Braswell\". Enabling flash write... OK.\n\ + 8388608\n" + ), + Ok(8388608) + ); + + assert_eq!( + flashrom_extract_size("There was a catastrophic error."), + Err("Found no purely-numeric lines in flashrom output".into()) + ); + } + + #[test] + fn extract_flash_name() { + use super::extract_flash_name; + + assert_eq!( + extract_flash_name( + "coreboot table found at 0x7cc13000\n\ + Found chipset \"Intel Braswell\". Enabling flash write... OK.\n\ + vendor=\"Winbond\" name=\"W25Q64DW\"\n" + ), + Some(("Winbond", "W25Q64DW")) + ); + + assert_eq!( + extract_flash_name( + "vendor name is TEST\n\ + Something failed!" + ), + None + ) + } +} diff --git a/util/flashrom_tester/flashrom/src/flashromlib.rs b/util/flashrom_tester/flashrom/src/flashromlib.rs new file mode 100644 index 00000000..5e1747b1 --- /dev/null +++ b/util/flashrom_tester/flashrom/src/flashromlib.rs @@ -0,0 +1,184 @@ +// Copyright 2022, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use libflashrom::{Chip, Programmer}; + +use std::{cell::RefCell, convert::TryFrom, fs, path::Path}; + +use crate::{FlashChip, FlashromError}; + +#[derive(Debug)] +pub struct FlashromLib { + // RefCell required here to keep Flashrom trait immutable. + // Cant make Flashrom methods mut because WriteProtectState + // and TestEnv both keep a reference. + pub flashrom: RefCell<Chip>, + pub fc: FlashChip, +} + +impl FlashromLib { + pub fn new(fc: FlashChip, log_level: libflashrom::flashrom_log_level) -> FlashromLib { + libflashrom::set_log_level(Some(log_level)); + let (programmer, options) = FlashChip::to_split(fc); + let flashrom = Chip::new(Programmer::new(programmer, options).unwrap(), None).unwrap(); + FlashromLib { + flashrom: RefCell::new(flashrom), + fc, + } + } +} + +impl crate::Flashrom for FlashromLib { + fn get_size(&self) -> Result<i64, FlashromError> { + Ok(self.flashrom.borrow().get_size() as i64) + } + + fn name(&self) -> Result<(String, String), FlashromError> { + Ok(("not".to_string(), "implemented".to_string())) + } + + fn wp_range(&self, range: (i64, i64), wp_enable: bool) -> Result<bool, FlashromError> { + let mut cfg = libflashrom::WriteProtectCfg::new()?; + let start = usize::try_from(range.0).unwrap(); + let len = usize::try_from(range.1).unwrap(); + cfg.set_range::<std::ops::Range<usize>>(start..(start + len)); + cfg.set_mode(if wp_enable { + libflashrom::flashrom_wp_mode::FLASHROM_WP_MODE_HARDWARE + } else { + libflashrom::flashrom_wp_mode::FLASHROM_WP_MODE_DISABLED + }); + self.flashrom.borrow_mut().set_wp(&cfg)?; + Ok(true) + } + + fn wp_list(&self) -> Result<String, FlashromError> { + let ranges = self.flashrom.borrow_mut().get_wp_ranges()?; + Ok(format!("{:?}", ranges)) + } + + fn wp_status(&self, en: bool) -> Result<bool, FlashromError> { + let ret = self + .flashrom + .borrow_mut() + .get_wp() + .map_err(|e| format!("{:?}", e))? + .get_mode(); + if en { + Ok(ret != libflashrom::flashrom_wp_mode::FLASHROM_WP_MODE_DISABLED) + } else { + Ok(ret == libflashrom::flashrom_wp_mode::FLASHROM_WP_MODE_DISABLED) + } + } + + fn wp_toggle(&self, en: bool) -> Result<bool, FlashromError> { + let range = if en { (0, self.get_size()?) } else { (0, 0) }; + self.wp_range(range, en) + } + + fn read_into_file(&self, path: &Path) -> Result<(), FlashromError> { + let buf = self.flashrom.borrow_mut().image_read(None)?; + fs::write(path, buf).map_err(|error| error.to_string())?; + Ok(()) + } + + fn read_region_into_file(&self, path: &Path, region: &str) -> Result<(), FlashromError> { + let mut layout = self.flashrom.borrow_mut().layout_read_fmap_from_rom()?; + layout.include_region(region)?; + let range = layout.get_region_range(region)?; + let buf = self.flashrom.borrow_mut().image_read(None)?; + fs::write(path, &buf[range]).map_err(|error| error.to_string())?; + Ok(()) + } + + fn write_from_file(&self, path: &Path) -> Result<(), FlashromError> { + let mut buf = fs::read(path).map_err(|error| error.to_string())?; + self.flashrom.borrow_mut().image_write(&mut buf, None)?; + Ok(()) + } + + fn write_from_file_region( + &self, + path: &Path, + region: &str, + layout: &Path, + ) -> Result<bool, FlashromError> { + let buf = fs::read(layout).map_err(|error| error.to_string())?; + let buf = String::from_utf8(buf).unwrap(); + let mut layout: libflashrom::Layout = buf + .parse() + .map_err(|e: Box<dyn std::error::Error>| e.to_string())?; + layout.include_region(region)?; + let mut buf = fs::read(path).map_err(|error| error.to_string())?; + self.flashrom + .borrow_mut() + .image_write(&mut buf, Some(layout))?; + Ok(true) + } + + fn verify_from_file(&self, path: &Path) -> Result<(), FlashromError> { + let buf = fs::read(path).map_err(|error| error.to_string())?; + self.flashrom.borrow_mut().image_verify(&buf, None)?; + Ok(()) + } + + fn verify_region_from_file(&self, path: &Path, region: &str) -> Result<(), FlashromError> { + let mut layout = self.flashrom.borrow_mut().layout_read_fmap_from_rom()?; + layout.include_region(region)?; + let range = layout.get_region_range(region)?; + let region_data = fs::read(path).map_err(|error| error.to_string())?; + if region_data.len() != range.len() { + return Err(format!( + "verify region range ({}) does not match provided file size ({})", + range.len(), + region_data.len() + ) + .into()); + } + let mut buf = vec![0; self.get_size()? as usize]; + buf[range].copy_from_slice(®ion_data); + self.flashrom + .borrow_mut() + .image_verify(&buf, Some(layout))?; + Ok(()) + } + + fn erase(&self) -> Result<(), FlashromError> { + self.flashrom.borrow_mut().erase()?; + Ok(()) + } + + fn can_control_hw_wp(&self) -> bool { + self.fc.can_control_hw_wp() + } +} diff --git a/util/flashrom_tester/flashrom/src/lib.rs b/util/flashrom_tester/flashrom/src/lib.rs new file mode 100644 index 00000000..41393e84 --- /dev/null +++ b/util/flashrom_tester/flashrom/src/lib.rs @@ -0,0 +1,165 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +#[macro_use] +extern crate log; + +mod cmd; +mod flashromlib; + +use std::{error, fmt, path::Path}; + +pub use cmd::FlashromCmd; +pub use flashromlib::FlashromLib; + +pub use libflashrom::{ + flashrom_log_level, FLASHROM_MSG_DEBUG, FLASHROM_MSG_DEBUG2, FLASHROM_MSG_ERROR, + FLASHROM_MSG_INFO, FLASHROM_MSG_SPEW, FLASHROM_MSG_WARN, +}; + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum FlashChip { + HOST, +} + +impl FlashChip { + pub fn from(s: &str) -> Result<FlashChip, &str> { + match s { + "host" => Ok(FlashChip::HOST), + _ => Err("cannot convert str to enum"), + } + } + pub fn to(fc: FlashChip) -> &'static str { + match fc { + FlashChip::HOST => "host", + } + } + + /// Return the programmer string and optional programmer options + pub fn to_split(fc: FlashChip) -> (&'static str, Option<&'static str>) { + let programmer = FlashChip::to(fc); + let mut bits = programmer.splitn(2, ':'); + (bits.next().unwrap(), bits.next()) + } + + /// Return whether the hardware write protect signal can be controlled. + /// + /// Servo and dediprog adapters are assumed to always have hardware write protect + /// disabled. + pub fn can_control_hw_wp(&self) -> bool { + match self { + FlashChip::HOST => true, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct FlashromError { + msg: String, +} + +impl fmt::Display for FlashromError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.msg) + } +} + +impl error::Error for FlashromError {} + +impl<T> From<T> for FlashromError +where + T: Into<String>, +{ + fn from(msg: T) -> Self { + FlashromError { msg: msg.into() } + } +} + +pub trait Flashrom { + /// Returns the size of the flash in bytes. + fn get_size(&self) -> Result<i64, FlashromError>; + + /// Returns the vendor name and the flash name. + fn name(&self) -> Result<(String, String), FlashromError>; + + /// Set write protect status and range. + fn wp_range(&self, range: (i64, i64), wp_enable: bool) -> Result<bool, FlashromError>; + + /// Read the write protect regions for the flash. + fn wp_list(&self) -> Result<String, FlashromError>; + + /// Return true if the flash write protect status matches `en`. + fn wp_status(&self, en: bool) -> Result<bool, FlashromError>; + + /// Set write protect status. + /// If en=true sets wp_range to the whole chip (0,getsize()). + /// If en=false sets wp_range to (0,0). + /// This is due to the MTD driver, which requires wp enable to use a range + /// length != 0 and wp disable to have the range 0,0. + fn wp_toggle(&self, en: bool) -> Result<bool, FlashromError>; + + /// Read the whole flash to the file specified by `path`. + fn read_into_file(&self, path: &Path) -> Result<(), FlashromError>; + + /// Read only a region of the flash into the file specified by `path`. Note + /// the first byte written to the file is the first byte from the region. + fn read_region_into_file(&self, path: &Path, region: &str) -> Result<(), FlashromError>; + + /// Write the whole flash to the file specified by `path`. + fn write_from_file(&self, path: &Path) -> Result<(), FlashromError>; + + /// Write only a region of the flash. + /// `path` is a file of the size of the whole flash. + /// The `region` name corresponds to a region name in the `layout` file, not the flash. + fn write_from_file_region( + &self, + path: &Path, + region: &str, + layout: &Path, + ) -> Result<bool, FlashromError>; + + /// Verify the whole flash against the file specified by `path`. + fn verify_from_file(&self, path: &Path) -> Result<(), FlashromError>; + + /// Verify only the region against the file specified by `path`. + /// Note the first byte in the file is matched against the first byte of the region. + fn verify_region_from_file(&self, path: &Path, region: &str) -> Result<(), FlashromError>; + + /// Erase the whole flash. + fn erase(&self) -> Result<(), FlashromError>; + + /// Return true if the hardware write protect of this flash can be controlled. + fn can_control_hw_wp(&self) -> bool; +} diff --git a/util/flashrom_tester/flashrom_remote.sh b/util/flashrom_tester/flashrom_remote.sh new file mode 100755 index 00000000..8dba9430 --- /dev/null +++ b/util/flashrom_tester/flashrom_remote.sh @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/bin/ssh localhost -p 60024 -C /usr/sbin/flashrom "$@" diff --git a/util/flashrom_tester/src/cros_sysinfo.rs b/util/flashrom_tester/src/cros_sysinfo.rs new file mode 100644 index 00000000..37e1ec65 --- /dev/null +++ b/util/flashrom_tester/src/cros_sysinfo.rs @@ -0,0 +1,70 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::ffi::OsStr; +use std::fs; +use std::io::Result as IoResult; +use std::process::{Command, Stdio}; + +use super::utils; + +fn dmidecode_dispatch<S: AsRef<OsStr>>(args: &[S]) -> IoResult<String> { + let output = Command::new("/usr/sbin/dmidecode") + .args(args) + .stdin(Stdio::null()) + .output()?; + + if !output.status.success() { + return Err(utils::translate_command_error(&output)); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +pub fn system_info() -> IoResult<String> { + dmidecode_dispatch(&["-q", "-t1"]) +} + +pub fn bios_info() -> IoResult<String> { + dmidecode_dispatch(&["-q", "-t0"]) +} + +pub fn release_description() -> IoResult<String> { + for l in fs::read_to_string("/etc/lsb-release")?.lines() { + if l.starts_with("CHROMEOS_RELEASE_DESCRIPTION") { + return Ok(l.to_string()); + } + } + Err(std::io::ErrorKind::NotFound.into()) +} diff --git a/util/flashrom_tester/src/lib.rs b/util/flashrom_tester/src/lib.rs new file mode 100644 index 00000000..d8f1cb6a --- /dev/null +++ b/util/flashrom_tester/src/lib.rs @@ -0,0 +1,46 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +#[macro_use] +extern crate log; + +#[macro_use] +pub mod types; + +pub mod cros_sysinfo; +pub mod rand_util; +pub mod tester; +pub mod tests; +pub mod utils; diff --git a/util/flashrom_tester/src/logger.rs b/util/flashrom_tester/src/logger.rs new file mode 100644 index 00000000..c9c36406 --- /dev/null +++ b/util/flashrom_tester/src/logger.rs @@ -0,0 +1,149 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use flashrom_tester::types; +use std::io::Write; + +struct Logger { + level: log::LevelFilter, + color: types::Color, +} + +impl log::Log for Logger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + fn log(&self, record: &log::Record) { + // Write errors deliberately ignored + let stdout = std::io::stdout(); + let mut lock = stdout.lock(); + let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true); + let _ = write!(lock, "{}{} ", self.color.magenta, now); + let _ = write!( + lock, + "{}[ {} ]{} ", + self.color.yellow, + record.level(), + self.color.reset + ); + let _ = writeln!(lock, "{}", record.args()); + } + + fn flush(&self) { + // Flush errors deliberately ignored + let _ = std::io::stdout().flush(); + } +} + +pub fn init(debug: bool) { + let mut logger = Logger { + level: log::LevelFilter::Info, + color: if atty::is(atty::Stream::Stdout) { + types::COLOR + } else { + types::NOCOLOR + }, + }; + + if debug { + logger.level = log::LevelFilter::Debug; + } + log::set_max_level(logger.level); + log::set_boxed_logger(Box::new(logger)).unwrap(); +} + +#[cfg(test)] +mod tests { + use std::io::Read; + + use super::Logger; + use flashrom_tester::types; + use log::{Level, LevelFilter, Log, Record}; + + fn run_records(records: &[Record]) -> String { + let buf = gag::BufferRedirect::stdout().unwrap(); + { + let logger = Logger { + level: LevelFilter::Info, + color: types::COLOR, + }; + + for record in records { + if logger.enabled(record.metadata()) { + logger.log(record); + } + } + } + let mut ret = String::new(); + buf.into_inner().read_to_string(&mut ret).unwrap(); + ret + } + + /// Log messages have the expected format + #[test] + fn format() { + let buf = run_records(&[Record::builder() + .args(format_args!("Test message at INFO")) + .level(Level::Info) + .build()]); + + assert_eq!(&buf[..5], "\x1b[35m"); + // Time is difficult to test, assume it's formatted okay + // Split on the UTC timezone char + assert_eq!( + buf.split_once("Z ").unwrap().1, + "\x1b[33m[ INFO ]\x1b[0m Test message at INFO\n" + ); + } + + #[test] + fn level_filter() { + let buf = run_records(&[ + Record::builder() + .args(format_args!("Test message at DEBUG")) + .level(Level::Debug) + .build(), + Record::builder() + .args(format_args!("Hello, world!")) + .level(Level::Error) + .build(), + ]); + + // There is one line because the Debug record wasn't written. + println!("{}", buf); + assert_eq!(buf.lines().count(), 1); + } +} diff --git a/util/flashrom_tester/src/main.rs b/util/flashrom_tester/src/main.rs new file mode 100644 index 00000000..b8a2581a --- /dev/null +++ b/util/flashrom_tester/src/main.rs @@ -0,0 +1,211 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +#[macro_use] +extern crate log; + +mod logger; + +use clap::{App, Arg}; +use flashrom::{FlashChip, Flashrom, FlashromCmd, FlashromLib}; +use flashrom_tester::{tester, tests}; +use std::sync::atomic::AtomicBool; + +pub mod built_info { + include!(concat!(env!("OUT_DIR"), "/built.rs")); +} + +fn main() { + let matches = App::new("flashrom_tester") + .long_version(&*format!( + "{}-{}\n\ + Target: {}\n\ + Profile: {}\n\ + Features: {:?}\n\ + Build time: {}\n\ + Compiler: {}", + built_info::PKG_VERSION, + option_env!("VCSID").unwrap_or("<unknown>"), + built_info::TARGET, + built_info::PROFILE, + built_info::FEATURES, + built_info::BUILT_TIME_UTC, + built_info::RUSTC_VERSION, + )) + .arg( + Arg::with_name("libflashrom") + .long("libflashrom") + .takes_value(false) + .help("Test the flashrom library instead of a binary"), + ) + .arg( + Arg::with_name("flashrom_binary") + .long("flashrom_binary") + .short("b") + .takes_value(true) + .required_unless("libflashrom") + .conflicts_with("libflashrom") + .help("Path to flashrom binary to test"), + ) + .arg( + Arg::with_name("ccd_target_type") + .required(true) + .possible_values(&["host"]), + ) + .arg( + Arg::with_name("print-layout") + .short("l") + .long("print-layout") + .help("Print the layout file's contents before running tests"), + ) + .arg( + Arg::with_name("log_debug") + .short("d") + .long("debug") + .help("Write detailed logs, for debugging"), + ) + .arg( + Arg::with_name("output-format") + .short("f") + .long("output-format") + .help("Set the test report format") + .takes_value(true) + .case_insensitive(true) + .possible_values(&["pretty", "json"]) + .default_value("pretty"), + ) + .arg( + Arg::with_name("test_name") + .multiple(true) + .help("Names of individual tests to run (run all if unspecified)"), + ) + .get_matches(); + + logger::init(matches.is_present("log_debug")); + debug!("Args parsed and logging initialized OK"); + + debug!("Collecting crossystem info"); + let crossystem = + flashrom_tester::utils::collect_crosssystem(&[]).expect("could not run crossystem"); + + let ccd_type = FlashChip::from( + matches + .value_of("ccd_target_type") + .expect("ccd_target_type should be required"), + ) + .expect("ccd_target_type should admit only known types"); + + let cmd: Box<dyn Flashrom> = if matches.is_present("libflashrom") { + Box::new(FlashromLib::new( + ccd_type, + if matches.is_present("log_debug") { + flashrom::FLASHROM_MSG_DEBUG + } else { + flashrom::FLASHROM_MSG_WARN + }, + )) + } else { + Box::new(FlashromCmd { + path: matches + .value_of("flashrom_binary") + .expect("flashrom_binary is required") + .to_string(), + fc: ccd_type, + }) + }; + + let print_layout = matches.is_present("print-layout"); + let output_format = matches + .value_of("output-format") + .expect("output-format should have a default value") + .parse::<tester::OutputFormat>() + .expect("output-format is not a parseable OutputFormat"); + let test_names = matches.values_of("test_name"); + + if let Err(e) = tests::generic( + cmd.as_ref(), + ccd_type, + print_layout, + output_format, + test_names, + Some(handle_sigint()), + crossystem, + ) { + eprintln!("Failed to run tests: {:?}", e); + std::process::exit(1); + } +} + +/// Catch exactly one SIGINT, printing a message in response and setting a flag. +/// +/// The returned value is false by default, becoming true after a SIGINT is +/// trapped. +/// +/// Once a signal is trapped, the default behavior is restored (terminating +/// the process) for future signals. +fn handle_sigint() -> &'static AtomicBool { + use libc::c_int; + use std::sync::atomic::Ordering; + + unsafe { + let _ = libc::signal(libc::SIGINT, sigint_handler as libc::sighandler_t); + } + static TERMINATE_FLAG: AtomicBool = AtomicBool::new(false); + + extern "C" fn sigint_handler(_: c_int) { + const STDERR_FILENO: c_int = 2; + static MESSAGE: &[u8] = b" +WARNING: terminating tests prematurely may leave Flash in an inconsistent state, +rendering your machine unbootable. Testing will end on completion of the current +test, or press ^C again to exit immediately (possibly bricking your machine). +"; + + // Use raw write() because signal-safety is a very hard problem. Safe because this doesn't + // modify any memory. + let _ = unsafe { + libc::write( + STDERR_FILENO, + MESSAGE.as_ptr() as *const libc::c_void, + MESSAGE.len() as libc::size_t, + ) + }; + unsafe { + let _ = libc::signal(libc::SIGINT, libc::SIG_DFL); + } + TERMINATE_FLAG.store(true, Ordering::Release); + } + + &TERMINATE_FLAG +} diff --git a/util/flashrom_tester/src/rand_util.rs b/util/flashrom_tester/src/rand_util.rs new file mode 100644 index 00000000..a040c89a --- /dev/null +++ b/util/flashrom_tester/src/rand_util.rs @@ -0,0 +1,80 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::fs::File; +use std::io::prelude::*; +use std::io::BufWriter; +use std::path::Path; + +use rand::prelude::*; + +pub fn gen_rand_testdata(path: &Path, size: usize) -> std::io::Result<()> { + let mut buf = BufWriter::new(File::create(path)?); + + let mut a: Vec<u8> = vec![0; size]; + thread_rng().fill(a.as_mut_slice()); + + buf.write_all(a.as_slice())?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn gen_rand_testdata() { + use super::gen_rand_testdata; + + let path0 = Path::new("/tmp/idk_test00"); + let path1 = Path::new("/tmp/idk_test01"); + let sz = 1024; + + gen_rand_testdata(path0, sz).unwrap(); + gen_rand_testdata(path1, sz).unwrap(); + + let mut buf0 = Vec::new(); + let mut buf1 = Vec::new(); + + let mut f = File::open(path0).unwrap(); + let mut g = File::open(path1).unwrap(); + + f.read_to_end(&mut buf0).unwrap(); + g.read_to_end(&mut buf1).unwrap(); + + assert_ne!(buf0, buf1); + } +} diff --git a/util/flashrom_tester/src/tester.rs b/util/flashrom_tester/src/tester.rs new file mode 100644 index 00000000..4629c2eb --- /dev/null +++ b/util/flashrom_tester/src/tester.rs @@ -0,0 +1,567 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use super::rand_util; +use super::types; +use super::utils::{self, LayoutSizes}; +use flashrom::FlashromError; +use flashrom::{FlashChip, Flashrom}; +use serde_json::json; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; + +// type-signature comes from the return type of lib.rs workers. +type TestError = Box<dyn std::error::Error>; +pub type TestResult = Result<(), TestError>; + +pub struct TestEnv<'a> { + chip_type: FlashChip, + /// Flashrom instantiation information. + /// + /// Where possible, prefer to use methods on the TestEnv rather than delegating + /// to the raw flashrom functions. + pub cmd: &'a dyn Flashrom, + layout: LayoutSizes, + + pub wp: WriteProtectState<'a>, + /// The path to a file containing the flash contents at test start. + original_flash_contents: PathBuf, + /// The path to a file containing flash-sized random data + random_data: PathBuf, + /// The path to a file containing layout data. + pub layout_file: PathBuf, +} + +impl<'a> TestEnv<'a> { + pub fn create( + chip_type: FlashChip, + cmd: &'a dyn Flashrom, + print_layout: bool, + ) -> Result<Self, FlashromError> { + let rom_sz = cmd.get_size()?; + let out = TestEnv { + chip_type, + cmd, + layout: utils::get_layout_sizes(rom_sz)?, + wp: WriteProtectState::from_hardware(cmd, chip_type)?, + original_flash_contents: "/tmp/flashrom_tester_golden.bin".into(), + random_data: "/tmp/random_content.bin".into(), + layout_file: create_layout_file(rom_sz, Path::new("/tmp/"), print_layout), + }; + + info!("Stashing golden image for verification/recovery on completion"); + out.cmd.read_into_file(&out.original_flash_contents)?; + out.cmd.verify_from_file(&out.original_flash_contents)?; + + info!("Generating random flash-sized data"); + rand_util::gen_rand_testdata(&out.random_data, rom_sz as usize) + .map_err(|io_err| format!("I/O error writing random data file: {:#}", io_err))?; + + Ok(out) + } + + pub fn run_test<T: TestCase>(&mut self, test: T) -> TestResult { + let name = test.get_name(); + info!("Beginning test: {}", name); + let out = test.run(self); + info!("Completed test: {}; result {:?}", name, out); + out + } + + pub fn chip_type(&self) -> FlashChip { + // This field is not public because it should be immutable to tests, + // so this getter enforces that it is copied. + self.chip_type + } + + /// Return the path to a file that contains random data and is the same size + /// as the flash chip. + pub fn random_data_file(&self) -> &Path { + &self.random_data + } + + pub fn layout(&self) -> &LayoutSizes { + &self.layout + } + + /// Return true if the current Flash contents are the same as the golden image + /// that was present at the start of testing. + pub fn is_golden(&self) -> bool { + self.cmd + .verify_from_file(&self.original_flash_contents) + .is_ok() + } + + /// Do whatever is necessary to make the current Flash contents the same as they + /// were at the start of testing. + pub fn ensure_golden(&mut self) -> Result<(), FlashromError> { + self.wp.set_hw(false)?.set_sw(false)?; + self.cmd.write_from_file(&self.original_flash_contents)?; + Ok(()) + } + + /// Attempt to erase the flash. + pub fn erase(&self) -> Result<(), FlashromError> { + self.cmd.erase()?; + Ok(()) + } + + /// Verify that the current Flash contents are the same as the file at the given + /// path. + /// + /// Returns Err if they are not the same. + pub fn verify(&self, contents_path: &Path) -> Result<(), FlashromError> { + self.cmd.verify_from_file(contents_path)?; + Ok(()) + } +} + +impl<'a> Drop for TestEnv<'a> { + fn drop(&mut self) { + info!("Verifying flash remains unmodified"); + if !self.is_golden() { + warn!("ROM seems to be in a different state at finish; restoring original"); + if let Err(e) = self.ensure_golden() { + error!("Failed to write back golden image: {:?}", e); + } + } + } +} + +struct WriteProtect { + hw: bool, + sw: bool, +} + +/// RAII handle for setting write protect in either hardware or software. +/// +/// Given an instance, the state of either write protect can be modified by calling +/// `set`. When it goes out of scope, the write protects will be returned +/// to the state they had then it was created. +pub struct WriteProtectState<'a> { + current: WriteProtect, + initial: WriteProtect, + cmd: &'a dyn Flashrom, + fc: FlashChip, +} + +impl<'a> WriteProtectState<'a> { + /// Initialize a state from the current state of the hardware. + pub fn from_hardware(cmd: &'a dyn Flashrom, fc: FlashChip) -> Result<Self, FlashromError> { + let hw = Self::get_hw(cmd)?; + let sw = Self::get_sw(cmd)?; + info!("Initial write protect state: HW={} SW={}", hw, sw); + + Ok(WriteProtectState { + current: WriteProtect { hw, sw }, + initial: WriteProtect { hw, sw }, + cmd, + fc, + }) + } + + /// Get the actual hardware write protect state. + fn get_hw(cmd: &dyn Flashrom) -> Result<bool, String> { + if cmd.can_control_hw_wp() { + super::utils::get_hardware_wp() + } else { + Ok(false) + } + } + + /// Get the actual software write protect state. + fn get_sw(cmd: &dyn Flashrom) -> Result<bool, FlashromError> { + let b = cmd.wp_status(true)?; + Ok(b) + } + + /// Return true if the current programmer supports setting the hardware + /// write protect. + /// + /// If false, calls to set_hw() will do nothing. + pub fn can_control_hw_wp(&self) -> bool { + self.cmd.can_control_hw_wp() + } + + /// Set the software write protect and check that the state is as expected. + pub fn set_sw(&mut self, enable: bool) -> Result<&mut Self, String> { + info!("set_sw request={}, current={}", enable, self.current.sw); + if self.current.sw != enable { + self.cmd + .wp_toggle(/* en= */ enable) + .map_err(|e| e.to_string())?; + } + if Self::get_sw(self.cmd).map_err(|e| e.to_string())? != enable { + Err(format!( + "Software write protect did not change state to {} when requested", + enable + )) + } else { + self.current.sw = enable; + Ok(self) + } + } + + // Set software write protect with a custom range + pub fn set_range(&mut self, range: (i64, i64), enable: bool) -> Result<&mut Self, String> { + info!("set_range request={}, current={}", enable, self.current.sw); + self.cmd + .wp_range(range, enable) + .map_err(|e| e.to_string())?; + let actual_state = Self::get_sw(self.cmd).map_err(|e| e.to_string())?; + if actual_state != enable { + Err(format!( + "set_range request={}, real={}", + enable, actual_state + )) + } else { + self.current.sw = enable; + Ok(self) + } + } + + /// Set the hardware write protect if supported and check that the state is as expected. + pub fn set_hw(&mut self, enable: bool) -> Result<&mut Self, String> { + info!("set_hw request={}, current={}", enable, self.current.hw); + if self.can_control_hw_wp() { + if self.current.hw != enable { + super::utils::toggle_hw_wp(/* dis= */ !enable)?; + } + // toggle_hw_wp does check this, but we might not have called toggle_hw_wp so check again. + if Self::get_hw(self.cmd)? != enable { + return Err(format!( + "Hardware write protect did not change state to {} when requested", + enable + )); + } + } else { + info!( + "Ignoring attempt to set hardware WP with {:?} programmer", + self.fc + ); + } + self.current.hw = enable; + Ok(self) + } + + /// Reset the hardware to what it was when this state was created, reporting errors. + /// + /// This behaves exactly like allowing a state to go out of scope, but it can return + /// errors from that process rather than panicking. + pub fn close(mut self) -> Result<(), String> { + let out = self.drop_internal(); + // We just ran drop, don't do it again + std::mem::forget(self); + out + } + + /// Sets both write protects to the state they had when this state was created. + fn drop_internal(&mut self) -> Result<(), String> { + // Toggle both protects back to their initial states. + // Software first because we can't change it once hardware is enabled. + if self.set_sw(self.initial.sw).is_err() { + self.set_hw(false)?; + self.set_sw(self.initial.sw)?; + } + self.set_hw(self.initial.hw)?; + + Ok(()) + } +} + +impl<'a> Drop for WriteProtectState<'a> { + fn drop(&mut self) { + self.drop_internal() + .expect("Error while dropping WriteProtectState") + } +} + +pub trait TestCase { + fn get_name(&self) -> &str; + fn expected_result(&self) -> TestConclusion; + fn run(&self, env: &mut TestEnv) -> TestResult; +} + +impl<S: AsRef<str>, F: Fn(&mut TestEnv) -> TestResult> TestCase for (S, F) { + fn get_name(&self) -> &str { + self.0.as_ref() + } + + fn expected_result(&self) -> TestConclusion { + TestConclusion::Pass + } + + fn run(&self, env: &mut TestEnv) -> TestResult { + (self.1)(env) + } +} + +impl<T: TestCase + ?Sized> TestCase for &T { + fn get_name(&self) -> &str { + (*self).get_name() + } + + fn expected_result(&self) -> TestConclusion { + (*self).expected_result() + } + + fn run(&self, env: &mut TestEnv) -> TestResult { + (*self).run(env) + } +} + +#[allow(dead_code)] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum TestConclusion { + Pass, + Fail, + UnexpectedPass, + UnexpectedFail, +} + +pub struct ReportMetaData { + pub chip_name: String, + pub os_release: String, + pub cros_release: String, + pub system_info: String, + pub bios_info: String, +} + +fn decode_test_result(res: TestResult, con: TestConclusion) -> (TestConclusion, Option<TestError>) { + use TestConclusion::*; + + match (res, con) { + (Ok(_), Fail) => (UnexpectedPass, None), + (Err(e), Pass) => (UnexpectedFail, Some(e)), + _ => (Pass, None), + } +} + +fn create_layout_file(rom_sz: i64, tmp_dir: &Path, print_layout: bool) -> PathBuf { + info!("Calculate ROM partition sizes & Create the layout file."); + let layout_sizes = utils::get_layout_sizes(rom_sz).expect("Could not partition rom"); + + let layout_file = tmp_dir.join("layout.file"); + let mut f = File::create(&layout_file).expect("Could not create layout file"); + let mut buf: Vec<u8> = vec![]; + utils::construct_layout_file(&mut buf, &layout_sizes).expect("Could not construct layout file"); + + f.write_all(&buf).expect("Writing layout file failed"); + if print_layout { + info!( + "Dumping layout file as requested:\n{}", + String::from_utf8_lossy(&buf) + ); + } + layout_file +} + +pub fn run_all_tests<T, TS>( + chip: FlashChip, + cmd: &dyn Flashrom, + ts: TS, + terminate_flag: Option<&AtomicBool>, + print_layout: bool, +) -> Vec<(String, (TestConclusion, Option<TestError>))> +where + T: TestCase + Copy, + TS: IntoIterator<Item = T>, +{ + let mut env = + TestEnv::create(chip, cmd, print_layout).expect("Failed to set up test environment"); + + let mut results = Vec::new(); + for t in ts { + if terminate_flag + .map(|b| b.load(Ordering::Acquire)) + .unwrap_or(false) + { + break; + } + + let result = decode_test_result(env.run_test(t), t.expected_result()); + results.push((t.get_name().into(), result)); + } + results +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum OutputFormat { + Pretty, + Json, +} + +impl std::str::FromStr for OutputFormat { + type Err = (); + + fn from_str(s: &str) -> Result<Self, Self::Err> { + use OutputFormat::*; + + if s.eq_ignore_ascii_case("pretty") { + Ok(Pretty) + } else if s.eq_ignore_ascii_case("json") { + Ok(Json) + } else { + Err(()) + } + } +} + +pub fn collate_all_test_runs( + truns: &[(String, (TestConclusion, Option<TestError>))], + meta_data: ReportMetaData, + format: OutputFormat, +) { + match format { + OutputFormat::Pretty => { + let color = if atty::is(atty::Stream::Stdout) { + types::COLOR + } else { + types::NOCOLOR + }; + println!(); + println!(" ============================="); + println!(" ===== AVL qual RESULTS ===="); + println!(" ============================="); + println!(); + println!(" %---------------------------%"); + println!(" os release: {}", meta_data.os_release); + println!(" cros release: {}", meta_data.cros_release); + println!(" chip name: {}", meta_data.chip_name); + println!(" system info: \n{}", meta_data.system_info); + println!(" bios info: \n{}", meta_data.bios_info); + println!(" %---------------------------%"); + println!(); + + for trun in truns.iter() { + let (name, (result, error)) = trun; + if *result != TestConclusion::Pass { + println!( + " {} {}", + style!(format!(" <+> {} test:", name), color.bold, color), + style_dbg!(result, color.red, color) + ); + match error { + None => {} + Some(e) => info!(" - {} failure details:\n{}", name, e.to_string()), + }; + } else { + println!( + " {} {}", + style!(format!(" <+> {} test:", name), color.bold, color), + style_dbg!(result, color.green, color) + ); + } + } + println!(); + } + OutputFormat::Json => { + use serde_json::{Map, Value}; + + let mut all_pass = true; + let mut tests = Map::<String, Value>::new(); + for (name, (result, error)) in truns { + let passed = *result == TestConclusion::Pass; + all_pass &= passed; + + let error = match error { + Some(e) => Value::String(format!("{:#?}", e)), + None => Value::Null, + }; + + assert!( + !tests.contains_key(name), + "Found multiple tests named {:?}", + name + ); + tests.insert( + name.into(), + json!({ + "pass": passed, + "error": error, + }), + ); + } + + let json = json!({ + "pass": all_pass, + "metadata": { + "os_release": meta_data.os_release, + "chip_name": meta_data.chip_name, + "system_info": meta_data.system_info, + "bios_info": meta_data.bios_info, + }, + "tests": tests, + }); + println!("{:#}", json); + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn decode_test_result() { + use super::decode_test_result; + use super::TestConclusion::*; + + let (result, err) = decode_test_result(Ok(()), Pass); + assert_eq!(result, Pass); + assert!(err.is_none()); + + let (result, err) = decode_test_result(Ok(()), Fail); + assert_eq!(result, UnexpectedPass); + assert!(err.is_none()); + + let (result, err) = decode_test_result(Err("broken".into()), Pass); + assert_eq!(result, UnexpectedFail); + assert!(err.is_some()); + + let (result, err) = decode_test_result(Err("broken".into()), Fail); + assert_eq!(result, Pass); + assert!(err.is_none()); + } + + #[test] + fn output_format_round_trip() { + use super::OutputFormat::{self, *}; + + assert_eq!(format!("{:?}", Pretty).parse::<OutputFormat>(), Ok(Pretty)); + assert_eq!(format!("{:?}", Json).parse::<OutputFormat>(), Ok(Json)); + } +} diff --git a/util/flashrom_tester/src/tests.rs b/util/flashrom_tester/src/tests.rs new file mode 100644 index 00000000..721a789d --- /dev/null +++ b/util/flashrom_tester/src/tests.rs @@ -0,0 +1,398 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use super::cros_sysinfo; +use super::tester::{self, OutputFormat, TestCase, TestEnv, TestResult}; +use super::utils::{self, LayoutNames}; +use flashrom::{FlashChip, Flashrom}; +use std::collections::{HashMap, HashSet}; +use std::convert::TryInto; +use std::fs::{self, File}; +use std::io::BufRead; +use std::sync::atomic::AtomicBool; + +const ELOG_FILE: &str = "/tmp/elog.file"; +const FW_MAIN_B_PATH: &str = "/tmp/FW_MAIN_B.bin"; + +/// Iterate over tests, yielding only those tests with names matching filter_names. +/// +/// If filter_names is None, all tests will be run. None is distinct from Some(∅); +// Some(∅) runs no tests. +/// +/// Name comparisons are performed in lower-case: values in filter_names must be +/// converted to lowercase specifically. +/// +/// When an entry in filter_names matches a test, it is removed from that set. +/// This allows the caller to determine if any entries in the original set failed +/// to match any test, which may be user error. +fn filter_tests<'n, 't: 'n, T: TestCase>( + tests: &'t [T], + filter_names: &'n mut Option<HashSet<String>>, +) -> impl 'n + Iterator<Item = &'t T> { + tests.iter().filter(move |test| match filter_names { + // Accept all tests if no names are given + None => true, + Some(ref mut filter_names) => { + // Pop a match to the test name from the filter set, retaining the test + // if there was a match. + filter_names.remove(&test.get_name().to_lowercase()) + } + }) +} + +/// Run tests. +/// +/// Only returns an Error if there was an internal error; test failures are Ok. +/// +/// test_names is the case-insensitive names of tests to run; if None, then all +/// tests are run. Provided names that don't match any known test will be logged +/// as a warning. +#[allow(clippy::or_fun_call)] // This is used for to_string here and we don't care. +pub fn generic<'a, TN: Iterator<Item = &'a str>>( + cmd: &dyn Flashrom, + fc: FlashChip, + print_layout: bool, + output_format: OutputFormat, + test_names: Option<TN>, + terminate_flag: Option<&AtomicBool>, + crossystem: String, +) -> Result<(), Box<dyn std::error::Error>> { + utils::ac_power_warning(); + + info!("Record crossystem information.\n{}", crossystem); + + // Register tests to run: + let tests: &[&dyn TestCase] = &[ + &("Get_device_name", get_device_name_test), + &("Coreboot_ELOG_sanity", elog_sanity_test), + &("Host_is_ChromeOS", host_is_chrome_test), + &("WP_Region_List", wp_region_list_test), + &("Erase_and_Write", erase_write_test), + &("Fail_to_verify", verify_fail_test), + &("HWWP_Locks_SWWP", hwwp_locks_swwp_test), + &("Lock_top_quad", partial_lock_test(LayoutNames::TopQuad)), + &( + "Lock_bottom_quad", + partial_lock_test(LayoutNames::BottomQuad), + ), + &( + "Lock_bottom_half", + partial_lock_test(LayoutNames::BottomHalf), + ), + &("Lock_top_half", partial_lock_test(LayoutNames::TopHalf)), + ]; + + // Limit the tests to only those requested, unless none are requested + // in which case all tests are included. + let mut filter_names: Option<HashSet<String>> = + test_names.map(|names| names.map(|s| s.to_lowercase()).collect()); + let tests = filter_tests(tests, &mut filter_names); + + let chip_name = cmd + .name() + .map(|x| format!("vendor=\"{}\" name=\"{}\"", x.0, x.1)) + .unwrap_or("<Unknown chip>".into()); + + // ------------------------. + // Run all the tests and collate the findings: + let results = tester::run_all_tests(fc, cmd, tests, terminate_flag, print_layout); + + // Any leftover filtered names were specified to be run but don't exist + for leftover in filter_names.iter().flatten() { + warn!("No test matches filter name \"{}\"", leftover); + } + + let os_release = sys_info::os_release().unwrap_or("<Unknown OS>".to_string()); + let cros_release = cros_sysinfo::release_description() + .unwrap_or("<Unknown or not a ChromeOS release>".to_string()); + let system_info = cros_sysinfo::system_info().unwrap_or("<Unknown System>".to_string()); + let bios_info = cros_sysinfo::bios_info().unwrap_or("<Unknown BIOS>".to_string()); + + let meta_data = tester::ReportMetaData { + chip_name, + os_release, + cros_release, + system_info, + bios_info, + }; + tester::collate_all_test_runs(&results, meta_data, output_format); + Ok(()) +} + +/// Query the programmer and chip name. +/// Success means we got something back, which is good enough. +fn get_device_name_test(env: &mut TestEnv) -> TestResult { + env.cmd.name()?; + Ok(()) +} + +/// List the write-protectable regions of flash. +/// NOTE: This is not strictly a 'test' as it is allowed to fail on some platforms. +/// However, we will warn when it does fail. +fn wp_region_list_test(env: &mut TestEnv) -> TestResult { + match env.cmd.wp_list() { + Ok(list_str) => info!("\n{}", list_str), + Err(e) => warn!("{}", e), + }; + Ok(()) +} + +/// Verify that enabling hardware and software write protect prevents chip erase. +fn erase_write_test(env: &mut TestEnv) -> TestResult { + if !env.is_golden() { + info!("Memory has been modified; reflashing to ensure erasure can be detected"); + env.ensure_golden()?; + } + + // With write protect enabled erase should fail. + env.wp.set_sw(true)?.set_hw(true)?; + if env.erase().is_ok() { + info!("Flashrom returned Ok but this may be incorrect; verifying"); + if !env.is_golden() { + return Err("Hardware write protect asserted however can still erase!".into()); + } + info!("Erase claimed to succeed but verify is Ok; assume erase failed"); + } + + // With write protect disabled erase should succeed. + env.wp.set_hw(false)?.set_sw(false)?; + env.erase()?; + if env.is_golden() { + return Err("Successful erase didn't modify memory".into()); + } + + Ok(()) +} + +/// Verify that enabling hardware write protect prevents disabling software write protect. +fn hwwp_locks_swwp_test(env: &mut TestEnv) -> TestResult { + if !env.wp.can_control_hw_wp() { + return Err("Lock test requires ability to control hardware write protect".into()); + } + + env.wp.set_hw(false)?.set_sw(true)?; + // Toggling software WP off should work when hardware WP is off. + // Then enable software WP again for the next test. + env.wp.set_sw(false)?.set_sw(true)?; + + // Toggling software WP off should not work when hardware WP is on. + env.wp.set_hw(true)?; + if env.wp.set_sw(false).is_ok() { + return Err("Software WP was reset despite hardware WP being enabled".into()); + } + Ok(()) +} + +/// Check that the elog contains *something*, as an indication that Coreboot +/// is actually able to write to the Flash. This only makes sense for chips +/// running Coreboot, which we assume is just host. +fn elog_sanity_test(env: &mut TestEnv) -> TestResult { + if env.chip_type() != FlashChip::HOST { + info!("Skipping ELOG sanity check for non-host chip"); + return Ok(()); + } + // flash should be back in the golden state + env.ensure_golden()?; + + const ELOG_RW_REGION_NAME: &str = "RW_ELOG"; + env.cmd + .read_region_into_file(ELOG_FILE.as_ref(), ELOG_RW_REGION_NAME)?; + + // Just checking for the magic numer + // TODO: improve this test to read the events + if fs::metadata(ELOG_FILE)?.len() < 4 { + return Err("ELOG contained no data".into()); + } + let data = fs::read(ELOG_FILE)?; + if u32::from_le_bytes(data[0..4].try_into()?) != 0x474f4c45 { + return Err("ELOG had bad magic number".into()); + } + Ok(()) +} + +/// Check that we are running ChromiumOS. +fn host_is_chrome_test(_env: &mut TestEnv) -> TestResult { + let release_info = if let Ok(f) = File::open("/etc/os-release") { + let buf = std::io::BufReader::new(f); + parse_os_release(buf.lines().flatten()) + } else { + info!("Unable to read /etc/os-release to probe system information"); + HashMap::new() + }; + + match release_info.get("ID") { + Some(id) if id == "chromeos" || id == "chromiumos" => Ok(()), + oid => { + let id = match oid { + Some(s) => s, + None => "UNKNOWN", + }; + Err(format!( + "Test host os-release \"{}\" should be but is not chromeos", + id + ) + .into()) + } + } +} + +/// Verify that software write protect for a range protects only the requested range. +fn partial_lock_test(section: LayoutNames) -> impl Fn(&mut TestEnv) -> TestResult { + move |env: &mut TestEnv| { + // Need a clean image for verification + env.ensure_golden()?; + + let (wp_section_name, start, len) = utils::layout_section(env.layout(), section); + // Disable hardware WP so we can modify the protected range. + env.wp.set_hw(false)?; + // Then enable software WP so the range is enforced and enable hardware + // WP so that flashrom does not disable software WP during the + // operation. + env.wp.set_range((start, len), true)?; + env.wp.set_hw(true)?; + + // Check that we cannot write to the protected region. + if env + .cmd + .write_from_file_region(env.random_data_file(), wp_section_name, &env.layout_file) + .is_ok() + { + return Err( + "Section should be locked, should not have been overwritable with random data" + .into(), + ); + } + if !env.is_golden() { + return Err("Section didn't lock, has been overwritten with random data!".into()); + } + + // Check that we can write to the non protected region. + let (non_wp_section_name, _, _) = + utils::layout_section(env.layout(), section.get_non_overlapping_section()); + env.cmd.write_from_file_region( + env.random_data_file(), + non_wp_section_name, + &env.layout_file, + )?; + + Ok(()) + } +} + +/// Check that flashrom 'verify' will fail if the provided data does not match the chip data. +fn verify_fail_test(env: &mut TestEnv) -> TestResult { + env.ensure_golden()?; + // Verify that verify is Ok when the data matches. We verify only a region + // and not the whole chip because coprocessors or firmware may have written + // some data in other regions. + env.cmd + .read_region_into_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?; + env.cmd + .verify_region_from_file(FW_MAIN_B_PATH.as_ref(), "FW_MAIN_B")?; + + // Verify that verify is false when the data does not match + match env.verify(env.random_data_file()) { + Ok(_) => Err("Verification says flash is full of random data".into()), + Err(_) => Ok(()), + } +} + +/// Ad-hoc parsing of os-release(5); mostly according to the spec, +/// but ignores quotes and escaping. +fn parse_os_release<I: IntoIterator<Item = String>>(lines: I) -> HashMap<String, String> { + fn parse_line(line: String) -> Option<(String, String)> { + if line.is_empty() || line.starts_with('#') { + return None; + } + + let delimiter = match line.find('=') { + Some(idx) => idx, + None => { + warn!("os-release entry seems malformed: {:?}", line); + return None; + } + }; + Some(( + line[..delimiter].to_owned(), + line[delimiter + 1..].to_owned(), + )) + } + + lines.into_iter().filter_map(parse_line).collect() +} + +#[test] +fn test_parse_os_release() { + let lines = [ + "BUILD_ID=12516.0.0", + "# this line is a comment followed by an empty line", + "", + "ID_LIKE=chromiumos", + "ID=chromeos", + "VERSION=79", + "EMPTY_VALUE=", + ]; + let map = parse_os_release(lines.iter().map(|&s| s.to_owned())); + + fn get<'a, 'b>(m: &'a HashMap<String, String>, k: &'b str) -> Option<&'a str> { + m.get(k).map(|s| s.as_ref()) + } + + assert_eq!(get(&map, "ID"), Some("chromeos")); + assert_eq!(get(&map, "BUILD_ID"), Some("12516.0.0")); + assert_eq!(get(&map, "EMPTY_VALUE"), Some("")); + assert_eq!(get(&map, ""), None); +} + +#[test] +fn test_name_filter() { + let test_one = ("Test One", |_: &mut TestEnv| Ok(())); + let test_two = ("Test Two", |_: &mut TestEnv| Ok(())); + let tests: &[&dyn TestCase] = &[&test_one, &test_two]; + + let mut names = None; + // All tests pass through + assert_eq!(filter_tests(tests, &mut names).count(), 2); + + names = Some(["test two"].iter().map(|s| s.to_string()).collect()); + // Filtered out test one + assert_eq!(filter_tests(tests, &mut names).count(), 1); + + names = Some(["test three"].iter().map(|s| s.to_string()).collect()); + // No tests emitted + assert_eq!(filter_tests(tests, &mut names).count(), 0); + // Name got left behind because no test matched it + assert_eq!(names.unwrap().len(), 1); +} diff --git a/util/flashrom_tester/src/types.rs b/util/flashrom_tester/src/types.rs new file mode 100644 index 00000000..9cefb27e --- /dev/null +++ b/util/flashrom_tester/src/types.rs @@ -0,0 +1,72 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +pub struct Color { + pub bold: &'static str, + pub reset: &'static str, + pub magenta: &'static str, + pub yellow: &'static str, + pub green: &'static str, + pub red: &'static str, +} + +pub const COLOR: Color = Color { + bold: "\x1b[1m", + reset: "\x1b[0m", + magenta: "\x1b[35m", + yellow: "\x1b[33m", + green: "\x1b[92m", + red: "\x1b[31m", +}; + +pub const NOCOLOR: Color = Color { + bold: "", + reset: "", + magenta: "", + yellow: "", + green: "", + red: "", +}; + +macro_rules! style_dbg { + ($s: expr, $c: expr, $col: expr) => { + format!("{}{:?}{}", $c, $s, $col.reset) + }; +} +macro_rules! style { + ($s: expr, $c: expr, $col: expr) => { + format!("{}{}{}", $c, $s, $col.reset) + }; +} diff --git a/util/flashrom_tester/src/utils.rs b/util/flashrom_tester/src/utils.rs new file mode 100644 index 00000000..4e8dd7cc --- /dev/null +++ b/util/flashrom_tester/src/utils.rs @@ -0,0 +1,241 @@ +// +// Copyright 2019, Google Inc. +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// Alternatively, this software may be distributed under the terms of the +// GNU General Public License ("GPL") version 2 as published by the Free +// Software Foundation. +// + +use std::io::prelude::*; +use std::process::Command; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum LayoutNames { + TopQuad, + TopHalf, + BottomHalf, + BottomQuad, +} + +impl LayoutNames { + // Return a section that does not overlap + pub fn get_non_overlapping_section(&self) -> LayoutNames { + match self { + LayoutNames::TopQuad => LayoutNames::BottomQuad, + LayoutNames::TopHalf => LayoutNames::BottomHalf, + LayoutNames::BottomHalf => LayoutNames::TopHalf, + LayoutNames::BottomQuad => LayoutNames::TopQuad, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct LayoutSizes { + half_sz: i64, + quad_sz: i64, + rom_top: i64, + bottom_half_top: i64, + bottom_quad_top: i64, + top_quad_bottom: i64, +} + +pub fn get_layout_sizes(rom_sz: i64) -> Result<LayoutSizes, String> { + if rom_sz <= 0 { + return Err("invalid rom size provided".into()); + } + if rom_sz & (rom_sz - 1) != 0 { + return Err("invalid rom size, not a power of 2".into()); + } + Ok(LayoutSizes { + half_sz: rom_sz / 2, + quad_sz: rom_sz / 4, + rom_top: rom_sz - 1, + bottom_half_top: (rom_sz / 2) - 1, + bottom_quad_top: (rom_sz / 4) - 1, + top_quad_bottom: (rom_sz / 4) * 3, + }) +} + +pub fn layout_section(ls: &LayoutSizes, ln: LayoutNames) -> (&'static str, i64, i64) { + match ln { + LayoutNames::TopQuad => ("TOP_QUAD", ls.top_quad_bottom, ls.quad_sz), + LayoutNames::TopHalf => ("TOP_HALF", ls.half_sz, ls.half_sz), + LayoutNames::BottomHalf => ("BOTTOM_HALF", 0, ls.half_sz), + LayoutNames::BottomQuad => ("BOTTOM_QUAD", 0, ls.quad_sz), + } +} + +pub fn construct_layout_file<F: Write>(mut target: F, ls: &LayoutSizes) -> std::io::Result<()> { + writeln!(target, "000000:{:x} BOTTOM_QUAD", ls.bottom_quad_top)?; + writeln!(target, "000000:{:x} BOTTOM_HALF", ls.bottom_half_top)?; + writeln!(target, "{:x}:{:x} TOP_HALF", ls.half_sz, ls.rom_top)?; + writeln!(target, "{:x}:{:x} TOP_QUAD", ls.top_quad_bottom, ls.rom_top) +} + +pub fn toggle_hw_wp(dis: bool) -> Result<(), String> { + // The easist way to toggle the hardware write-protect is + // to {dis}connect the battery (and/or open the WP screw). + let s = if dis { "dis" } else { "" }; + // Print a failure message, but not on the first try. + let mut fail_msg = None; + while dis == get_hardware_wp()? { + if let Some(msg) = fail_msg { + eprintln!("{msg}"); + } + fail_msg = Some(format!("Hardware write protect is still {}!", !dis)); + // The following message is read by the tast test. Do not modify. + info!("Prompt for hardware WP {}able", s); + eprintln!(" > {}connect the battery (and/or open the WP screw)", s); + pause(); + } + Ok(()) +} + +pub fn ac_power_warning() { + info!("*****************************"); + info!("AC power *must be* connected!"); + info!("*****************************"); + pause(); +} + +fn pause() { + // The following message is read by the tast test. Do not modify. + println!("Press enter to continue..."); + // Rust stdout is always LineBuffered at time of writing. + // But this is not guaranteed, so flush anyway. + std::io::stdout().flush().unwrap(); + // This reads one line, there is no guarantee the line came + // after the above prompt. But it is good enough. + if std::io::stdin().read_line(&mut String::new()).unwrap() == 0 { + panic!("stdin closed"); + } +} + +pub fn get_hardware_wp() -> std::result::Result<bool, String> { + let wp_s_val = collect_crosssystem(&["wpsw_cur"])?.parse::<u32>(); + match wp_s_val { + Ok(v) => { + if v == 1 { + Ok(true) + } else if v == 0 { + Ok(false) + } else { + Err("Unknown write protect value".into()) + } + } + Err(_) => Err("Cannot parse write protect value".into()), + } +} + +pub fn collect_crosssystem(args: &[&str]) -> Result<String, String> { + let cmd = match Command::new("crossystem").args(args).output() { + Ok(x) => x, + Err(e) => return Err(format!("Failed to run crossystem: {}", e)), + }; + + if !cmd.status.success() { + return Err(translate_command_error(&cmd).to_string()); + }; + + Ok(String::from_utf8_lossy(&cmd.stdout).into_owned()) +} + +pub fn translate_command_error(output: &std::process::Output) -> std::io::Error { + use std::io::{Error, ErrorKind}; + // There is two cases on failure; + // i. ) A bad exit code, + // ii.) A SIG killed us. + match output.status.code() { + Some(code) => { + let e = format!( + "{}\nExited with error code: {}", + String::from_utf8_lossy(&output.stderr), + code + ); + Error::new(ErrorKind::Other, e) + } + None => Error::new( + ErrorKind::Other, + "Process terminated by a signal".to_string(), + ), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn construct_layout_file() { + use super::{construct_layout_file, get_layout_sizes}; + + let mut buf = Vec::new(); + construct_layout_file( + &mut buf, + &get_layout_sizes(0x10000).expect("64k is a valid chip size"), + ) + .expect("no I/O errors expected"); + + assert_eq!( + &buf[..], + &b"000000:3fff BOTTOM_QUAD\n\ + 000000:7fff BOTTOM_HALF\n\ + 8000:ffff TOP_HALF\n\ + c000:ffff TOP_QUAD\n"[..] + ); + } + + #[test] + fn get_layout_sizes() { + use super::get_layout_sizes; + + assert_eq!( + get_layout_sizes(-128).err(), + Some("invalid rom size provided".into()) + ); + + assert_eq!( + get_layout_sizes(3 << 20).err(), + Some("invalid rom size, not a power of 2".into()) + ); + + assert_eq!( + get_layout_sizes(64 << 10).unwrap(), + LayoutSizes { + half_sz: 0x8000, + quad_sz: 0x4000, + rom_top: 0xFFFF, + bottom_half_top: 0x7FFF, + bottom_quad_top: 0x3FFF, + top_quad_bottom: 0xC000, + } + ); + } +} |