| // Copyright lowRISC contributors. |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| use crate::util::parse_int::ParseInt; |
| use anyhow::{ensure, Result}; |
| use std::fs::File; |
| use std::io::Read; |
| use std::path::{Path, PathBuf}; |
| use thiserror::Error; |
| |
| #[derive(Debug, Error)] |
| pub enum ImageError { |
| #[error("Incomplete read: expected to read {0} bytes but read {1} bytes")] |
| IncompleteRead(usize, usize), |
| } |
| |
| #[derive(Debug)] |
| pub enum ImageChunk { |
| Concat(PathBuf), |
| Offset(PathBuf, usize), |
| } |
| |
| #[derive(Debug, Default)] |
| pub struct ImageAssembler { |
| pub size: usize, |
| pub mirrored: bool, |
| pub chunks: Vec<ImageChunk>, |
| } |
| |
| impl ImageAssembler { |
| /// Creates an `ImageAssembler` with a given `size` and mirroring parameters. |
| pub fn with_params(size: usize, mirrored: bool) -> Self { |
| ImageAssembler { |
| size, |
| mirrored, |
| ..Default::default() |
| } |
| } |
| |
| /// Creates an `ImageAssembler` with default parameters for OpenTitan: a 1MiB image which is mirrored. |
| pub fn new() -> Self { |
| Self::with_params(0x100000, true) |
| } |
| |
| /// Parse a list of strings into chunks to be assembled. |
| /// Each string may be a filename or a filename@offset describing where in the assembled image the contents of the file should appear. |
| /// The offset is an integer expressed in any of the bases accepted by [`ParseInt`]. |
| pub fn parse(&mut self, chunks: &[impl AsRef<str>]) -> Result<()> { |
| for chunk in chunks { |
| if let Some((file, offset)) = chunk.as_ref().split_once('@') { |
| self.chunks.push(ImageChunk::Offset( |
| PathBuf::from(file), |
| usize::from_str(offset)?, |
| )); |
| } else { |
| self.chunks |
| .push(ImageChunk::Concat(PathBuf::from(chunk.as_ref()))); |
| } |
| } |
| Ok(()) |
| } |
| |
| // Read a file into a buffer. Ensure the entire file is read. |
| fn read(path: &Path, buf: &mut [u8]) -> Result<usize> { |
| let mut file = File::open(path)?; |
| let len = file.metadata()?.len() as usize; |
| let n = file.read(buf)?; |
| ensure!(len == n, ImageError::IncompleteRead(len, n)); |
| Ok(n) |
| } |
| |
| /// Assemble the image according to the parameters and parsed chunk specifications. |
| pub fn assemble(&self) -> Result<Vec<u8>> { |
| let size = if self.mirrored { |
| self.size / 2 |
| } else { |
| self.size |
| }; |
| let mut image = vec![0xff; size]; |
| let mut pos = 0; |
| for chunk in &self.chunks { |
| match chunk { |
| ImageChunk::Concat(path) => { |
| let n = Self::read(path, &mut image[pos..])?; |
| pos += n; |
| } |
| ImageChunk::Offset(path, offset) => { |
| let n = Self::read(path, &mut image[*offset..])?; |
| pos = offset + n; |
| } |
| } |
| } |
| if self.mirrored { |
| image.extend_from_within(..size); |
| } |
| Ok(image) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| fn testdata(s: &str) -> String { |
| let mut result = "sw/host/opentitanlib/src/util/testdata/".to_string(); |
| result.push_str(s); |
| result |
| } |
| |
| #[test] |
| fn test_assemble_concat() -> Result<()> { |
| // Test image assembly by concatenation. |
| let mut image = ImageAssembler::with_params(16, false); |
| image.parse(&[testdata("hello.txt"), testdata("world.txt")])?; |
| let data = image.assemble()?; |
| assert_eq!(data, b"HelloWorld\xff\xff\xff\xff\xff\xff"); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_assemble_offset() -> Result<()> { |
| // Test image assembly by explicit offsets. |
| let mut image = ImageAssembler::with_params(16, false); |
| image.parse(&[ |
| testdata("hello.txt@0"), |
| testdata("world.txt@0x8"), |
| ])?; |
| let data = image.assemble()?; |
| assert_eq!(data, b"Hello\xff\xff\xffWorld\xff\xff\xff"); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_assemble_mirrored() -> Result<()> { |
| // Test image assembly with mirroring. |
| let mut image = ImageAssembler::with_params(20, true); |
| image.parse(&[testdata("hello.txt"), testdata("world.txt")])?; |
| let data = image.assemble()?; |
| assert_eq!(data, b"HelloWorldHelloWorld"); |
| Ok(()) |
| } |
| |
| #[test] |
| fn test_assemble_mirrored_offset_error() -> Result<()> { |
| // Test image assembly where one of the source files isn't read completely. |
| let mut image = ImageAssembler::with_params(16, true); |
| image.parse(&[ |
| testdata("hello.txt@0"), |
| testdata("world.txt@0x5"), |
| ])?; |
| let err = image.assemble().unwrap_err(); |
| assert_eq!( |
| err.to_string(), |
| "Incomplete read: expected to read 5 bytes but read 3 bytes" |
| ); |
| Ok(()) |
| } |
| } |