blob: c215998deeb8f287ec09da1c66f25a27026057cb [file] [log] [blame]
// 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(())
}
}