mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-06-02 06:23:34 +02:00
Add support for packing and unpacking Android sparse images
This supports all features of Android sparse images, including holes, and CRC32 (both full image checksum and CRC32 chunks). Partial sparse images, like those included in GrapheneOS' new optimized factory images, can also be packed and unpacked with these new commands, unlike AOSP's simg2img and img2simg tools. This new functionality is not relevant for avbroot's main use case, but is useful for unpacking certain factory images for comparison with OTAs during troubleshooting. Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Generated
+46
@@ -131,7 +131,9 @@ dependencies = [
|
||||
"cms",
|
||||
"const-oid",
|
||||
"constcat",
|
||||
"crc32fast",
|
||||
"ctrlc",
|
||||
"dlv-list",
|
||||
"flate2",
|
||||
"gf256",
|
||||
"hex",
|
||||
@@ -412,6 +414,26 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constcat"
|
||||
version = "0.5.0"
|
||||
@@ -461,6 +483,12 @@ version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
@@ -552,6 +580,15 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||
dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "3.6.0"
|
||||
@@ -1694,6 +1731,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.1"
|
||||
|
||||
+45
-1
@@ -286,7 +286,7 @@ All metadata slots in the newly packed LP image will be identical.
|
||||
### Repacking an LP image
|
||||
|
||||
```bash
|
||||
avbroot lp repack [-i <input LP image>] [-i <input LP image>]... -o <output LP image> [-o <output LP image>]...
|
||||
avbroot lp repack -i <input LP image> [-i <input LP image>]... -o <output LP image> [-o <output LP image>]...
|
||||
```
|
||||
|
||||
This subcommand is logically equivalent to `avbroot lp unpack` followed by `avbroot lp pack`, except more efficient. Instead of unpacking and packing all partition images, the raw data is directly copied from the old LP image to the new LP image.
|
||||
@@ -340,3 +340,47 @@ avbroot payload info -i <payload>
|
||||
```
|
||||
|
||||
This subcommand shows all of the payload header fields (which will likely be extremely long).
|
||||
|
||||
## `avbroot sparse`
|
||||
|
||||
This set of commands is for working with Android sparse images. All features of the file format are supported, including hole chunks and CRC32 checksums.
|
||||
|
||||
### Unpacking a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse unpack -o <input sparse image> -o <output raw image>
|
||||
```
|
||||
|
||||
This subcommand unpacks a sparse image to a raw image. If the sparse image contains CRC32 checksums, they will be validated during unpacking. If the sparse image contains holes, the output image will be created as a native sparse file.
|
||||
|
||||
Certain fastboot factory images may have multiple sparse images, like `super_1.img`, `super_2.img`, etc., where they all touch a disjoint set of regions on the same partition. These can be unpacked by running this subcommand for each sparse image and specifying the `--preserve` option along with using the same output file. This preserves the existing data in the output file when unpacking each sparse image.
|
||||
|
||||
### Packing a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse pack -i <input raw image> -o <output sparse image>
|
||||
```
|
||||
|
||||
This subcommand packs a new sparse image from a raw image. The default block size is 4096 bytes, which can be changed with the `--block-size` option.
|
||||
|
||||
By default, this will pack the entire input file. However, on Linux, there is an optimization where all holes in the input file, if it is a native sparse file, will be stored as hole chunks instead of `0`-filled chunks in the output sparse image.
|
||||
|
||||
To pack a partial sparse image, such as those used in the special fastboot factory images mentioned above, pass in `--region <start> <end>`. This option can be specified multiple times to pack multiple regions.
|
||||
|
||||
Unlike AOSP's `img2simg` tool, which never writes CRC32 checksums, this subcommand will write checksums if the input file has no holes and the entire file is being packed.
|
||||
|
||||
### Repacking a sparse image
|
||||
|
||||
```bash
|
||||
avbroot sparse repack -i <input sparse image> -o <output sparse image>
|
||||
```
|
||||
|
||||
This subcommand is logically equivalent to `avbroot sparse unpack` followed by `avbroot sparse pack`, except more efficient. This is useful for roundtrip testing of avbroot's sparse file parser.
|
||||
|
||||
### Showing sparse image metadata
|
||||
|
||||
```bash
|
||||
avbroot sparse info -i <input sparse image>
|
||||
```
|
||||
|
||||
This subcommand shows the sparse image metadata, including the header and all chunks.
|
||||
|
||||
@@ -20,7 +20,9 @@ clap = { version = "4.4.1", features = ["derive"] }
|
||||
clap_complete = "4.4.0"
|
||||
cms = { version = "0.2.2", features = ["std"] }
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
dlv-list = "0.5.2"
|
||||
flate2 = "1.0.27"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
|
||||
@@ -15,7 +15,7 @@ use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -30,6 +30,7 @@ pub enum Command {
|
||||
Lp(lp::LpCli),
|
||||
Ota(ota::OtaCli),
|
||||
Payload(payload::PayloadCli),
|
||||
Sparse(sparse::SparseCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
@@ -134,6 +135,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
|
||||
Command::Lp(c) => lp::lp_main(&c, cancel_signal),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
|
||||
Command::Sparse(c) => sparse::sparse_main(&c, cancel_signal),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
|
||||
@@ -14,3 +14,4 @@ pub mod key;
|
||||
pub mod lp;
|
||||
pub mod ota;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{little_endian, AsBytes};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
padding,
|
||||
sparse::{
|
||||
self, Chunk, ChunkBounds, ChunkData, ChunkList, CrcMode, Header, SparseReader,
|
||||
SparseWriter,
|
||||
},
|
||||
},
|
||||
stream,
|
||||
};
|
||||
|
||||
struct CompactView<'a, T>(&'a [T]);
|
||||
|
||||
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
|
||||
for item in self.0 {
|
||||
// No alternate mode for no inner newlines.
|
||||
list.entry(&format_args!("{item:?}"));
|
||||
}
|
||||
|
||||
list.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Metadata {
|
||||
header: Header,
|
||||
chunks: Vec<Chunk>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Metadata {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Metadata")
|
||||
.field("header", &self.header)
|
||||
.field("chunks", &CompactView(&self.chunks))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn open_reader(path: &Path) -> Result<File> {
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))
|
||||
}
|
||||
|
||||
fn open_writer(path: &Path, truncate: bool) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(truncate)
|
||||
.open(path)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))
|
||||
}
|
||||
|
||||
fn display_metadata(cli: &SparseCli, metadata: &Metadata) {
|
||||
if !cli.quiet {
|
||||
println!("{metadata:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits large data chunks to ensure that none exceed 64 MiB. This is not
|
||||
/// necessary in most cases, but is kept to match the behavior of AOSP's
|
||||
/// libsparse.
|
||||
fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
|
||||
const MAX_BYTES: u32 = 64 * 1024 * 1024;
|
||||
|
||||
let max_blocks_per_chunk = MAX_BYTES / block_size;
|
||||
let mut result = vec![];
|
||||
|
||||
for mut chunk in chunks.iter().copied() {
|
||||
if chunk.data == ChunkData::Data {
|
||||
while chunk.bounds.len() > max_blocks_per_chunk {
|
||||
result.push(Chunk {
|
||||
bounds: ChunkBounds {
|
||||
start: chunk.bounds.start,
|
||||
end: chunk.bounds.start + max_blocks_per_chunk,
|
||||
},
|
||||
data: chunk.data,
|
||||
});
|
||||
|
||||
chunk.bounds.start += max_blocks_per_chunk;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(chunk);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// [Linux only] Find allocated regions of the file. This avoids needing to read
|
||||
/// unused portions of the file if it is a native sparse file.
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn find_allocated_regions(
|
||||
path: &Path,
|
||||
reader: &mut File,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
use rustix::{fs::SeekFrom, io::Errno};
|
||||
|
||||
let mut result = vec![];
|
||||
let mut start;
|
||||
let mut end = 0;
|
||||
|
||||
loop {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
|
||||
Ok(offset) => offset,
|
||||
Err(e) if e == Errno::NXIO => break,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
|
||||
};
|
||||
|
||||
end = rustix::fs::seek(&*reader, SeekFrom::Hole(start as i64))
|
||||
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
|
||||
|
||||
result.push(start..end);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compute chunk boundaries for the list of potentially overlapping file byte
|
||||
/// regions. If `exact_bounds` is true, then the regions must be block-aligned.
|
||||
/// Otherwise, the lower boundaries are aligned down and the upper boundaries
|
||||
/// are aligned up.
|
||||
fn get_chunks_for_regions(
|
||||
block_size: u32,
|
||||
file_size: u64,
|
||||
file_regions: &[Range<u64>],
|
||||
exact_bounds: bool,
|
||||
) -> Result<(u32, Vec<ChunkBounds>)> {
|
||||
let block_size_64 = u64::from(block_size);
|
||||
|
||||
let file_blocks: u32 = (file_size / u64::from(block_size))
|
||||
.try_into()
|
||||
.map_err(|_| anyhow!("File size {file_size} too large for block size {block_size}"))?;
|
||||
|
||||
let mut chunk_list = ChunkList::new();
|
||||
chunk_list.set_len(file_blocks);
|
||||
|
||||
for region in file_regions {
|
||||
let mut start_byte = region.start;
|
||||
let mut end_byte = region.end;
|
||||
|
||||
if exact_bounds {
|
||||
if start_byte % block_size_64 != 0 || end_byte % block_size_64 != 0 {
|
||||
bail!("File region bounds are not block-aligned: {region:?}");
|
||||
}
|
||||
} else {
|
||||
start_byte = start_byte / block_size_64 * block_size_64;
|
||||
end_byte = padding::round(end_byte, block_size_64).unwrap();
|
||||
}
|
||||
|
||||
let start_block: u32 = (start_byte / block_size_64).try_into().map_err(|_| {
|
||||
anyhow!("Region start offset {start_byte} too large for block size {block_size}")
|
||||
})?;
|
||||
let end_block: u32 = (end_byte / block_size_64).try_into().map_err(|_| {
|
||||
anyhow!("Region end offset {end_byte} too large for block size {block_size}")
|
||||
})?;
|
||||
|
||||
chunk_list.insert_data(ChunkBounds {
|
||||
start: start_block,
|
||||
end: end_block,
|
||||
});
|
||||
}
|
||||
|
||||
let chunks = chunk_list.iter_allocated().map(|c| c.bounds).collect();
|
||||
|
||||
Ok((file_blocks, chunks))
|
||||
}
|
||||
|
||||
/// Compute the sparse [`Chunk`]s needed to cover the specified regions.
|
||||
fn compute_chunks(
|
||||
path: &Path,
|
||||
reader: &mut File,
|
||||
block_size: u32,
|
||||
file_blocks: u32,
|
||||
block_regions: &[ChunkBounds],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(ChunkList, u32)> {
|
||||
let mut chunk_list = ChunkList::new();
|
||||
let mut hasher = Some(Hasher::new());
|
||||
let mut buf = vec![0u8; block_size as usize];
|
||||
let mut block = 0;
|
||||
|
||||
chunk_list.set_len(file_blocks);
|
||||
|
||||
for bounds in block_regions {
|
||||
if bounds.start != block {
|
||||
// Not contiguous so we cannot compute the checksum.
|
||||
hasher = None;
|
||||
}
|
||||
|
||||
let offset = u64::from(bounds.start) * u64::from(block_size);
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
for block in *bounds {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.with_context(|| format!("Failed to read full block: {path:?}"))?;
|
||||
|
||||
if let Some(h) = &mut hasher {
|
||||
h.update(&buf);
|
||||
}
|
||||
|
||||
let new_bounds = ChunkBounds {
|
||||
start: block,
|
||||
end: block + 1,
|
||||
};
|
||||
|
||||
if buf.chunks_exact(4).all(|c| c == &buf[..4]) {
|
||||
let fill_value = u32::from_le_bytes(buf[..4].try_into().unwrap());
|
||||
chunk_list.insert_fill(new_bounds, fill_value);
|
||||
} else {
|
||||
chunk_list.insert_data(new_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
block = bounds.end;
|
||||
}
|
||||
|
||||
if block != file_blocks {
|
||||
hasher = None;
|
||||
}
|
||||
|
||||
let crc32 = hasher.map(|h| h.finalize()).unwrap_or_default();
|
||||
|
||||
Ok((chunk_list, crc32))
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
let mut writer = open_writer(&cli.output, !cli.preserve)?;
|
||||
|
||||
if cli.preserve {
|
||||
let expected_size =
|
||||
u64::from(metadata.header.num_blocks) * u64::from(metadata.header.block_size);
|
||||
let file_size = writer
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.output))?;
|
||||
|
||||
if file_size < expected_size {
|
||||
writer
|
||||
.set_len(expected_size)
|
||||
.with_context(|| format!("Failed to set file size: {:?}", cli.output))?;
|
||||
}
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Start(0))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
match chunk.data {
|
||||
ChunkData::Fill(value) => {
|
||||
let fill_value = little_endian::U32::from(value);
|
||||
let buf = vec![fill_value; metadata.header.block_size as usize / 4];
|
||||
|
||||
for _ in chunk.bounds {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
writer
|
||||
.write_all(buf.as_bytes())
|
||||
.with_context(|| format!("Failed to write data: {:?}", cli.output))?;
|
||||
}
|
||||
}
|
||||
ChunkData::Data => {
|
||||
// This cannot overflow.
|
||||
let to_copy = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
stream::copy_n(
|
||||
&mut sparse_reader,
|
||||
&mut writer,
|
||||
to_copy.into(),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| {
|
||||
format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output)
|
||||
})?;
|
||||
}
|
||||
ChunkData::Hole => {
|
||||
// This cannot overflow.
|
||||
let to_skip = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Current(to_skip.into()))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
ChunkData::Crc32(_) => {}
|
||||
}
|
||||
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
sparse_reader
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &PackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
if cli.block_size == 0 || cli.block_size % 4 != 0 {
|
||||
bail!(
|
||||
"Block size must be a non-zero multiple of 4: {}",
|
||||
cli.block_size,
|
||||
);
|
||||
}
|
||||
|
||||
let mut reader = open_reader(&cli.input)?;
|
||||
|
||||
let file_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
|
||||
if file_size % u64::from(cli.block_size) != 0 {
|
||||
bail!(
|
||||
"File size {file_size} is not a multiple of block size {}",
|
||||
cli.block_size,
|
||||
);
|
||||
}
|
||||
|
||||
// Compute the byte regions to pack into the sparse file.
|
||||
let (file_regions, exact_bounds) = if !cli.region.is_empty() {
|
||||
let regions = cli
|
||||
.region
|
||||
.chunks_exact(2)
|
||||
.map(|c| c[0]..c[1])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(regions, false)
|
||||
} else {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
let regions = find_allocated_regions(&cli.input, &mut reader, cancel_signal)?;
|
||||
|
||||
(regions, false)
|
||||
}
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
{
|
||||
(vec![0..file_size], true)
|
||||
}
|
||||
};
|
||||
|
||||
// Get the file regions as non-overlapping and sorted block regions.
|
||||
let (file_blocks, block_regions) =
|
||||
get_chunks_for_regions(cli.block_size, file_size, &file_regions, exact_bounds)?;
|
||||
|
||||
// Compute the checksum (if possible) and the list of actual chunks.
|
||||
let (chunk_list, crc32) = compute_chunks(
|
||||
&cli.input,
|
||||
&mut reader,
|
||||
cli.block_size,
|
||||
file_blocks,
|
||||
&block_regions,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
let chunks = split_chunks(&chunk_list.to_chunks(), cli.block_size);
|
||||
let metadata = Metadata {
|
||||
header: Header {
|
||||
major_version: sparse::MAJOR_VERSION,
|
||||
minor_version: sparse::MINOR_VERSION,
|
||||
block_size: cli.block_size,
|
||||
num_blocks: chunk_list.len(),
|
||||
// This can't overflow because the number of chunks is always
|
||||
// smaller than the number of blocks (because we don't add CRC32
|
||||
// chunks).
|
||||
num_chunks: chunks.len() as u32,
|
||||
// This will be zero if the regions don't span the entire file.
|
||||
crc32,
|
||||
},
|
||||
chunks,
|
||||
};
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
let writer = open_writer(&cli.output, true)?;
|
||||
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
|
||||
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
|
||||
|
||||
for chunk in metadata.chunks {
|
||||
sparse_writer
|
||||
.start_chunk(chunk)
|
||||
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
|
||||
|
||||
if chunk.data == ChunkData::Data {
|
||||
let offset = u64::from(chunk.bounds.start) * u64::from(cli.block_size);
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.input))?;
|
||||
|
||||
let to_copy = u64::from(chunk.bounds.len()) * u64::from(cli.block_size);
|
||||
|
||||
stream::copy_n(&mut reader, &mut sparse_writer, to_copy, cancel_signal).with_context(
|
||||
|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
sparse_writer
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(
|
||||
sparse_cli: &SparseCli,
|
||||
cli: &RepackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Validate)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
let writer = open_writer(&cli.output, true)?;
|
||||
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
|
||||
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
sparse_writer
|
||||
.start_chunk(chunk)
|
||||
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
|
||||
|
||||
if chunk.data == ChunkData::Data {
|
||||
// This cannot overflow.
|
||||
let to_copy = chunk.bounds.len() * metadata.header.block_size;
|
||||
|
||||
stream::copy_n(
|
||||
&mut sparse_reader,
|
||||
&mut sparse_writer,
|
||||
to_copy.into(),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output))?;
|
||||
}
|
||||
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
sparse_reader
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
|
||||
sparse_writer
|
||||
.finish()
|
||||
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(sparse_cli: &SparseCli, cli: &InfoCli) -> Result<()> {
|
||||
let reader = open_reader(&cli.input)?;
|
||||
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Ignore)
|
||||
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
|
||||
|
||||
let mut metadata = Metadata {
|
||||
header: sparse_reader.header(),
|
||||
chunks: vec![],
|
||||
};
|
||||
|
||||
while let Some(chunk) = sparse_reader
|
||||
.next_chunk()
|
||||
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
|
||||
{
|
||||
metadata.chunks.push(chunk);
|
||||
}
|
||||
|
||||
display_metadata(sparse_cli, &metadata);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sparse_main(cli: &SparseCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
SparseCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
SparseCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack a sparse image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output raw image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Preserve existing data in the output file.
|
||||
///
|
||||
/// This is useful when unpacking multiple sparse files into a single output
|
||||
/// file because they contain disjoint blocks of data.
|
||||
#[arg(long)]
|
||||
preserve: bool,
|
||||
}
|
||||
|
||||
/// Pack a sparse image.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output sparse image.
|
||||
///
|
||||
/// If `--region` is not used and the input file is not a (native) sparse
|
||||
/// file on Linux, then the output sparse image is written with a CRC32
|
||||
/// checksum in the header.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input raw image.
|
||||
///
|
||||
/// On Linux, if this is a (native) sparse file, then the unallocated
|
||||
/// sections of the file will be skipped and will be stored in the output
|
||||
/// file as hole chunks.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Block size.
|
||||
#[arg(short, long, value_name = "BYTES", default_value_t = 4096)]
|
||||
block_size: u32,
|
||||
|
||||
/// Pack certain byte regions from the file.
|
||||
///
|
||||
/// The start offset will be aligned down to the block size and the end
|
||||
/// offset will be aligned up. This option can be specified any number of
|
||||
/// times and in any order. Overlapping regions are allowed.
|
||||
///
|
||||
/// Unused regions will be stored in the sparse file as hole chunks.
|
||||
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
|
||||
region: Vec<u64>,
|
||||
}
|
||||
|
||||
/// Repack a sparse image.
|
||||
///
|
||||
/// This command is equivalent to running `unpack` and `pack`, except without
|
||||
/// storing the unpacked data to disk.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Display sparse image metadata.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input sparse image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum SparseCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect sparse images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SparseCli {
|
||||
#[command(subcommand)]
|
||||
command: SparseCommand,
|
||||
|
||||
/// Don't print sparse image metadata.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
@@ -166,7 +166,7 @@ const _: () = assert!(mem::size_of::<RawGeometry>() < GEOMETRY_SIZE as usize);
|
||||
impl fmt::Debug for RawGeometry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RawGeometry")
|
||||
.field("magic", &format_args!("{:#08x}", self.magic.get()))
|
||||
.field("magic", &format_args!("{:#010x}", self.magic.get()))
|
||||
.field("struct_size", &self.struct_size.get())
|
||||
.field("checksum", &hex::encode(self.checksum))
|
||||
.field("metadata_max_size", &self.metadata_max_size.get())
|
||||
@@ -182,7 +182,7 @@ impl RawGeometry {
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.magic.get() != GEOMETRY_MAGIC {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Invalid magic: {:#08x}",
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
}
|
||||
@@ -332,7 +332,7 @@ struct RawHeader {
|
||||
impl fmt::Debug for RawHeader {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("RawHeader")
|
||||
.field("magic", &format_args!("{:#08x}", self.magic.get()))
|
||||
.field("magic", &format_args!("{:#010x}", self.magic.get()))
|
||||
.field("major_version", &self.major_version.get())
|
||||
.field("minor_version", &self.minor_version.get())
|
||||
.field("header_size", &self.header_size.get())
|
||||
@@ -392,7 +392,7 @@ impl RawHeader {
|
||||
fn validate(&self, geometry: &RawGeometry) -> Result<()> {
|
||||
if self.magic.get() != HEADER_MAGIC {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid magic: {:#08x}",
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ pub mod lp;
|
||||
pub mod ota;
|
||||
pub mod padding;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
pub mod verityrs;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use avbroot::format::sparse::{
|
||||
self, Chunk, ChunkBounds, ChunkData, CrcMode, Header, SparseReader, SparseWriter,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct TestChunk {
|
||||
chunk: Chunk,
|
||||
data: &'static [u8],
|
||||
}
|
||||
|
||||
fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[u8; 64]) {
|
||||
let num_blocks = test_chunks.iter().map(|d| d.chunk.bounds.len()).sum();
|
||||
let header = Header {
|
||||
major_version: sparse::MAJOR_VERSION,
|
||||
minor_version: sparse::MINOR_VERSION,
|
||||
block_size,
|
||||
num_blocks,
|
||||
num_chunks: test_chunks.len() as u32,
|
||||
crc32,
|
||||
};
|
||||
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut sparse_writer = SparseWriter::new(writer, header).unwrap();
|
||||
|
||||
for test_chunk in test_chunks {
|
||||
sparse_writer.start_chunk(test_chunk.chunk).unwrap();
|
||||
|
||||
if !test_chunk.data.is_empty() {
|
||||
sparse_writer.write_all(test_chunk.data).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let writer = sparse_writer.finish().unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
let reader = Cursor::new(&data);
|
||||
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate).unwrap();
|
||||
|
||||
assert_eq!(sparse_reader.header(), header);
|
||||
|
||||
let mut test_chunks_iter = test_chunks.iter();
|
||||
|
||||
while let Some(chunk) = sparse_reader.next_chunk().unwrap() {
|
||||
let test_chunk = test_chunks_iter.next().unwrap();
|
||||
|
||||
assert_eq!(chunk, test_chunk.chunk);
|
||||
|
||||
if !test_chunk.data.is_empty() {
|
||||
let mut buf = vec![];
|
||||
sparse_reader.read_to_end(&mut buf).unwrap();
|
||||
|
||||
assert_eq!(buf, test_chunk.data);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(test_chunks_iter.next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_full_image() {
|
||||
let block_size = 8;
|
||||
let file_crc32 = 0xf6e23567;
|
||||
let test_chunks = [
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 0, end: 1 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 1 },
|
||||
data: ChunkData::Crc32(0x88aa689f),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 2 },
|
||||
data: ChunkData::Fill(0x01234567),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 2, end: 3 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 3, end: 3 },
|
||||
data: ChunkData::Crc32(0xf6e23567),
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
];
|
||||
let sha512 = [
|
||||
0x19, 0x5f, 0xa7, 0xdb, 0x18, 0xc6, 0xb9, 0x0e, 0xce, 0x4b, 0x4f, 0x35, 0x36, 0x79, 0x46,
|
||||
0x02, 0x7a, 0x45, 0x66, 0x63, 0x0e, 0xd9, 0x76, 0x93, 0x2b, 0x88, 0xe2, 0xbc, 0x0b, 0xd9,
|
||||
0x1f, 0x21, 0x51, 0x92, 0x00, 0x2e, 0xe3, 0xa2, 0xff, 0x24, 0xea, 0xef, 0x24, 0xd5, 0x24,
|
||||
0xf0, 0x46, 0xf3, 0x10, 0x32, 0xf4, 0xa6, 0x3b, 0x9d, 0xcd, 0xc5, 0x57, 0xf4, 0xc0, 0xe8,
|
||||
0x01, 0xe8, 0x1d, 0xb3,
|
||||
];
|
||||
|
||||
round_trip(block_size, file_crc32, &test_chunks, &sha512);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_partial_image() {
|
||||
let block_size = 8;
|
||||
let file_crc32 = 0;
|
||||
let test_chunks = [
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 0, end: 1 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 1, end: 2 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 2, end: 3 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 3, end: 4 },
|
||||
data: ChunkData::Data,
|
||||
},
|
||||
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
|
||||
},
|
||||
TestChunk {
|
||||
chunk: Chunk {
|
||||
bounds: ChunkBounds { start: 4, end: 5 },
|
||||
data: ChunkData::Hole,
|
||||
},
|
||||
data: b"",
|
||||
},
|
||||
];
|
||||
let sha512 = [
|
||||
0xee, 0x07, 0xc5, 0x4d, 0x85, 0xee, 0x69, 0x91, 0x61, 0x07, 0x10, 0xed, 0xec, 0x13, 0x5e,
|
||||
0xfb, 0xc3, 0x7d, 0xcf, 0x1f, 0x2a, 0x13, 0xf0, 0xb6, 0x85, 0xb4, 0xee, 0xe9, 0xd7, 0xa1,
|
||||
0x12, 0x79, 0x14, 0x16, 0x30, 0x7a, 0x81, 0xf9, 0x4f, 0x72, 0xb2, 0xdd, 0x33, 0xbe, 0x5d,
|
||||
0x55, 0x70, 0xa9, 0xe3, 0x94, 0x29, 0x40, 0x29, 0x8f, 0x35, 0x23, 0xf8, 0x78, 0x7f, 0xfe,
|
||||
0xd6, 0x4b, 0x60, 0x16,
|
||||
];
|
||||
|
||||
round_trip(block_size, file_crc32, &test_chunks, &sha512);
|
||||
}
|
||||
@@ -33,6 +33,7 @@ allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
use avbroot::format::sparse::{ChunkData, CrcMode, SparseReader};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let reader = Cursor::new(data);
|
||||
if let Ok(mut sparse_reader) = SparseReader::new(reader, CrcMode::Ignore) {
|
||||
while let Ok(Some(chunk)) = sparse_reader.next_chunk() {
|
||||
if chunk.data == ChunkData::Data {
|
||||
let _ = io::copy(&mut sparse_reader, &mut io::sink());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
Reference in New Issue
Block a user