summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYecheng Zhao <zyecheng@google.com>2024-04-01 19:55:31 +0000
committerYecheng Zhao <zyecheng@google.com>2024-04-02 21:06:52 +0000
commit7c5ac199a496595a584103ef42947f18e9a9b7b1 (patch)
tree516b7231936bc3cba59715b5cb7cbb2f16854c51
parentc837e6b1981989e2d09a4a0610e2545b20ae7522 (diff)
downloadlibbootloader-7c5ac199a496595a584103ef42947f18e9a9b7b1.tar.gz
Support flashing sparse image
Bug: 331854173 Change-Id: Ic8774f5fa0462138dcbb3a1bc093e5b98ff14612
-rw-r--r--gbl/libfastboot/src/lib.rs14
-rw-r--r--gbl/libgbl/BUILD4
-rw-r--r--gbl/libgbl/src/fastboot/mod.rs28
-rw-r--r--gbl/libgbl/src/fastboot/sparse.rs345
-rw-r--r--gbl/libgbl/testdata/gen_sparse_test_bin.py56
-rw-r--r--gbl/libgbl/testdata/sparse_test.binbin0 -> 24704 bytes
-rw-r--r--gbl/libgbl/testdata/sparse_test_blk1024.binbin0 -> 24704 bytes
-rw-r--r--gbl/libgbl/testdata/sparse_test_raw.binbin0 -> 57344 bytes
8 files changed, 440 insertions, 7 deletions
diff --git a/gbl/libfastboot/src/lib.rs b/gbl/libfastboot/src/lib.rs
index 5a67baf..2254154 100644
--- a/gbl/libfastboot/src/lib.rs
+++ b/gbl/libfastboot/src/lib.rs
@@ -403,10 +403,14 @@ impl<'a> FastbootUtils<'a> {
&mut self.download_buffer[..*self.download_data_size]
}
- /// Takes the download buffer. The downloaded data is invalidated.
- pub fn take_download_buffer(&mut self) -> &mut [u8] {
+ /// Returns the entire download buffer and the size of the download data. The method assumes
+ /// that callers will modify the download buffer and therefore will no longer consider the
+ /// download data valid, i.e. future calls of FastbootUtils::download_data() will only return
+ /// an empty slice.
+ pub fn take_download_buffer(&mut self) -> (&mut [u8], usize) {
+ let download_data_size = *self.download_data_size;
*self.download_data_size = 0;
- self.download_buffer
+ (self.download_buffer, download_data_size)
}
/// Sends a Fastboot INFO message.
@@ -1387,8 +1391,8 @@ mod test {
let mut upload_cb = |upload_builder: UploadBuilder, utils: &mut FastbootUtils| {
assert_eq!(utils.download_buffer.len(), DOWNLOAD_BUFFER_LEN);
assert_eq!(utils.download_data().to_vec(), download_content);
- let download_len = utils.download_data().len();
- let to_send = &mut utils.take_download_buffer()[..download_len];
+ let (download_buffer, download_len) = utils.take_download_buffer();
+ let to_send = &mut download_buffer[..download_len];
let mut uploader = upload_builder.start(u64::try_from(to_send.len()).unwrap()).unwrap();
uploader.upload(&to_send[..download_len / 2]).unwrap();
uploader.upload(&to_send[download_len / 2..]).unwrap();
diff --git a/gbl/libgbl/BUILD b/gbl/libgbl/BUILD
index 20cef67..a53b2ee 100644
--- a/gbl/libgbl/BUILD
+++ b/gbl/libgbl/BUILD
@@ -31,6 +31,7 @@ rust_library(
"@gbl//libstorage",
"@gbl//third_party/libzbi",
"@spin",
+ "@static_assertions",
"@zerocopy",
],
)
@@ -41,6 +42,9 @@ rust_test(
crate = ":libgbl",
crate_features = ["uuid"],
data = [
+ "@gbl//libgbl/testdata:sparse_test.bin",
+ "@gbl//libgbl/testdata:sparse_test_blk1024.bin",
+ "@gbl//libgbl/testdata:sparse_test_raw.bin",
"@gbl//libgbl/testdata:test_image.img",
"@gbl//libgbl/testdata:testkey_rsa4096.pem",
"@gbl//libgbl/testdata:testkey_rsa4096.pub.pem",
diff --git a/gbl/libgbl/src/fastboot/mod.rs b/gbl/libgbl/src/fastboot/mod.rs
index 0cc83aa..b0d533a 100644
--- a/gbl/libgbl/src/fastboot/mod.rs
+++ b/gbl/libgbl/src/fastboot/mod.rs
@@ -23,6 +23,9 @@ use gbl_storage::{AsBlockDevice, AsMultiBlockDevices, GPT_NAME_LEN_U16};
mod vars;
use vars::{BlockDevice, Partition, Variable};
+mod sparse;
+use sparse::{is_sparse_image, write_sparse_image};
+
pub(crate) const GPT_NAME_LEN_U8: usize = GPT_NAME_LEN_U16 * 2;
/// `GblFbPartition` represents a GBL Fastboot partition, which is defined as any sub window of a
@@ -191,7 +194,14 @@ impl FastbootImplementation for GblFastboot<'_> {
fn flash(&mut self, part: &str, utils: &mut FastbootUtils) -> Result<(), CommandError> {
let part = self.parse_partition(part.split(':'))?;
- self.partition_io(part).write(0, utils.download_data())
+ match is_sparse_image(utils.download_data()) {
+ // Passes the entire download buffer so that more can be used as fill buffer.
+ Ok(_) => write_sparse_image(utils.take_download_buffer().0, |off, data| {
+ self.partition_io(part).write(off, data)
+ })
+ .map(|_| ()),
+ _ => self.partition_io(part).write(0, utils.download_data()),
+ }
}
fn upload(
@@ -211,7 +221,7 @@ impl FastbootImplementation for GblFastboot<'_> {
utils: &mut FastbootUtils,
) -> Result<(), CommandError> {
let part = self.parse_partition(part.split(':'))?;
- let buffer = utils.take_download_buffer();
+ let (buffer, _) = utils.take_download_buffer();
let buffer_len = u64::try_from(buffer.len())
.map_err::<CommandError, _>(|_| "buffer size overflow".into())?;
let end = add(offset, size)?;
@@ -574,4 +584,18 @@ mod test {
check_flash_part(&mut gbl_fb, ":0:200", &mut disk_0[off..][..size]);
check_flash_part(&mut gbl_fb, ":1:200", &mut disk_1[off..][..size]);
}
+
+ #[test]
+ fn test_flash_partition_sparse() {
+ let raw = include_bytes!("../../testdata/sparse_test_raw.bin");
+ let sparse = include_bytes!("../../testdata/sparse_test.bin");
+ let mut devs = TestMultiBlockDevices(vec![test_block_device(&vec![0u8; raw.len()])]);
+ let mut fb = GblFastboot::new(&mut devs);
+
+ let mut dl_size = sparse.len();
+ let mut download = sparse.to_vec();
+ let mut utils = FastbootUtils::new(&mut download[..], &mut dl_size, None);
+ fb.flash(":0", &mut utils).unwrap();
+ assert_eq!(fetch(&mut fb, ":0".into(), 0, raw.len().try_into().unwrap()).unwrap(), raw);
+ }
}
diff --git a/gbl/libgbl/src/fastboot/sparse.rs b/gbl/libgbl/src/fastboot/sparse.rs
new file mode 100644
index 0000000..ec45b34
--- /dev/null
+++ b/gbl/libgbl/src/fastboot/sparse.rs
@@ -0,0 +1,345 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use core::cmp::{max, min};
+use core::mem::size_of;
+use fastboot::CommandError;
+
+use static_assertions::const_assert;
+use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref};
+
+// TODO(b/331854173): Switch to use bindgen for the following type definitions once
+// system/core/libsparse is added to repo checkout.
+
+const HEADER_MAGIC: u32 = 0xED26FF3A;
+const CHUNK_TYPE_RAW: u16 = 0xCAC1;
+const CHUNK_TYPE_FILL: u16 = 0xCAC2;
+const CHUNK_TYPE_DONT_CARE: u16 = 0xCAC3;
+const CHUNK_TYPE_CRC32: u16 = 0xCAC4;
+
+const SPARSE_HEADER_MAJOR_VER: u16 = 1;
+const SPARSE_HEADER_MINOR_VER: u16 = 0;
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone, AsBytes, FromBytes, FromZeroes)]
+pub struct SparseHeader {
+ pub magic: u32,
+ pub major_version: u16,
+ pub minor_version: u16,
+ pub file_hdr_sz: u16,
+ pub chunk_hdr_sz: u16,
+ pub blk_sz: u32,
+ pub total_blks: u32,
+ pub total_chunks: u32,
+ pub image_checksum: u32,
+}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone, AsBytes, FromBytes, FromZeroes)]
+pub struct ChunkHeader {
+ pub chunk_type: u16,
+ pub reserved1: u16,
+ pub chunk_sz: u32,
+ pub total_sz: u32,
+}
+
+const ERR_ARITHMETIC_OVERFLOW: &str = "Arithmetic Overflow";
+const ERR_IMAGE_SIZE: &str = "Bad image. Invalid image size";
+
+/// Checks if a sparse image is valid and returns the sparse header.
+pub fn is_sparse_image(sparse_img: &[u8]) -> Result<SparseHeader, CommandError> {
+ let sparse_header: SparseHeader = copy_from(sparse_img)?;
+ if sparse_header.magic != HEADER_MAGIC {
+ return Err("Sparse magic mismatch".into());
+ } else if sparse_header.major_version != SPARSE_HEADER_MAJOR_VER {
+ return Err("Sparse major version mismatch".into());
+ } else if sparse_header.minor_version != SPARSE_HEADER_MINOR_VER {
+ return Err("Sparse minor version mismatch".into());
+ }
+ Ok(sparse_header)
+}
+
+/// `FillInfo` is derived from a sparse chunk and contains information whether to fill a value or
+/// skip for a number of blocks.
+///
+/// Context and uses:
+///
+/// When writing fill chunks from a sparse image, it is usually better to write a larger buffer
+/// with the filled value instead of a single u32 at a time. However, separately maintaining a fill
+/// buffer can be inconvenient for the caller. Therefore, we use a strategy that re-uses the input
+/// buffer for fill buffer.
+///
+/// The idea is to write the sparse image in two passes. In the first pass, we only write non-fill
+/// chunks. For each sparse chunk, we create a `FillInfo` and append it from the beginning of the
+/// input buffer. For fill chunks, `FillInfo::fill_blocks` and
+/// `FillInfo::fill_value_or_skip_blocks` are set to the chunk size and fill value. For others,
+/// `FillInfo::fill_blocks` will be set to 0 and `FillInfo::fill_value_or_skip_blocks` will be set
+/// to the chunk size instead to represent number of blocks to skip. The second pass writes the
+/// fill chunk according to `FillInfo`.
+///
+/// Because a sparse chunk is at least 12 bytes, whereas `FillInfo` is 8 bytes, at the end of the
+/// first pass, we are guaranteed to have at least 1/3 of the input buffer free to use as fill
+/// buffer.
+#[repr(C, packed)]
+#[derive(Debug, Default, Copy, Clone, AsBytes, FromBytes, FromZeroes)]
+struct FillInfo {
+ // Number of blocks to fill.
+ pub fill_blocks: u32,
+ // If `fill_blocks` is None, this field represents the number of blocks to skip.
+ // Otherwise, it represents the fill value.
+ pub fill_value_or_skip_blocks: u32,
+}
+
+impl FillInfo {
+ /// Creates an instance that represents filling a number of blocks.
+ fn new_fill(blocks: u32, value: u32) -> Self {
+ assert_ne!(blocks, 0);
+ Self { fill_blocks: blocks, fill_value_or_skip_blocks: value }
+ }
+
+ /// Creates an instance that represents skipping a number of blocks.
+ fn new_skip(blocks: u32) -> Self {
+ Self { fill_blocks: 0, fill_value_or_skip_blocks: blocks }
+ }
+
+ // Returns (blocks, None) for the skip case or (blocks, Some(value)) for the fill case.
+ fn get_blocks_and_value(&self) -> (u32, Option<u32>) {
+ match self.fill_blocks {
+ 0 => (self.fill_value_or_skip_blocks, None),
+ v => (v, Some(self.fill_value_or_skip_blocks)),
+ }
+ }
+}
+
+const_assert!(size_of::<FillInfo>() < size_of::<ChunkHeader>());
+
+/// Write a sparse image in `sparse_img`.
+///
+/// # Args
+//
+/// * `sparse_img`: The input buffer containing the sparse image. The API modifes input buffer for
+/// internal optimization.
+/// * `write`: A closure as the writer. It takes an offset and a `&mut [u8]` as the write data.
+///
+/// # Returns
+///
+/// Returns the total number of bytes written, including don't care chunks.
+pub fn write_sparse_image<F>(sparse_img: &mut [u8], mut write: F) -> Result<u64, CommandError>
+where
+ F: FnMut(u64, &mut [u8]) -> Result<(), CommandError>,
+{
+ let sparse_header: SparseHeader = is_sparse_image(sparse_img)?;
+ let mut curr: usize = size_of::<SparseHeader>();
+ let mut write_offset = 0u64;
+
+ // First pass. Writes non-fill chunk and constructs `FillInfo`.
+ let mut fill_off = 0usize;
+ for _ in 0..sparse_header.total_chunks {
+ let header: ChunkHeader = copy_from(&mut sparse_img[curr..])?;
+ let payload = &mut sparse_img[curr + size_of::<ChunkHeader>()..];
+ let payload_sz = u64_mul(header.chunk_sz, sparse_header.blk_sz)?;
+ let mut fill = FillInfo::new_skip(header.chunk_sz);
+ match header.chunk_type {
+ CHUNK_TYPE_RAW => write(write_offset, get_mut(payload, 0, to_usize(payload_sz)?)?)?,
+ CHUNK_TYPE_FILL if header.chunk_sz != 0 => {
+ let fill_val = u32::from_le_bytes(get_mut(payload, 0, 4)?.try_into().unwrap());
+ fill = FillInfo::new_fill(header.chunk_sz, fill_val);
+ }
+ CHUNK_TYPE_DONT_CARE | CHUNK_TYPE_CRC32 => {}
+ _ => return Err("Invalid Chunk Type".into()),
+ };
+ write_offset = u64_add(write_offset, payload_sz)?;
+ sparse_img[fill_off..][..size_of::<FillInfo>()].clone_from_slice(fill.as_bytes());
+ fill_off = usize_add(fill_off, size_of::<FillInfo>())?;
+ curr = usize_add(curr, header.total_sz)?;
+ }
+ let total = write_offset;
+
+ // Second pass. Writes fill chunks.
+ // Use all reamining buffer as fill buffer.
+ let (fill_infos, fill_buffer) = sparse_img.split_at_mut(fill_off);
+ let mut fill_buffer = FillBuffer { curr_val: None, curr_size: 0, buffer: fill_buffer };
+ let fill_infos = Ref::<_, [FillInfo]>::new_slice(fill_infos).unwrap().into_slice();
+ write_offset = 0;
+ for ele in fill_infos {
+ match ele.get_blocks_and_value() {
+ (blks, None) => {
+ write_offset = u64_add(write_offset, u64_mul(blks, sparse_header.blk_sz)?)?;
+ }
+ (blks, Some(v)) => {
+ let sz = u64_mul(blks, sparse_header.blk_sz)?;
+ let buffer = fill_buffer.get(v, sz)?;
+ let buffer_len = to_u64(buffer.len())?;
+ let end = u64_add(write_offset, sz)?;
+ while write_offset < end {
+ let to_write = min(buffer_len, end - write_offset);
+ write(write_offset, &mut buffer[..to_usize(to_write).unwrap()])?;
+ write_offset += to_write;
+ }
+ }
+ }
+ }
+ Ok(total)
+}
+
+/// `FillUnit` is a packed C struct wrapping a u32. It is mainly used for filling a buffer of
+/// arbitrary alignment with a u32 value.
+#[repr(C, packed)]
+#[derive(Debug, Default, Copy, Clone, AsBytes, FromBytes, FromZeroes)]
+struct FillUnit(u32);
+
+/// `FillBuffer` manages a buffer and provides API for making a fill buffer with the given value.
+struct FillBuffer<'a> {
+ curr_val: Option<u32>,
+ curr_size: usize,
+ buffer: &'a mut [u8],
+}
+
+impl FillBuffer<'_> {
+ /// Get a buffer up to `size` number of bytes filled with `val`.
+ fn get(&mut self, val: u32, size: u64) -> Result<&mut [u8], CommandError> {
+ let aligned_len = self.buffer.len() - (self.buffer.len() % size_of::<u32>());
+ let size: usize = min(to_u64(aligned_len)?, size).try_into().unwrap();
+ if Some(val) != self.curr_val {
+ self.curr_size = 0;
+ self.curr_val = Some(val);
+ }
+ let gap = max(self.curr_size, size) - self.curr_size;
+ let to_fill = &mut self.buffer[self.curr_size..][..gap];
+ Ref::<_, [FillUnit]>::new_slice(to_fill).unwrap().into_mut_slice().fill(FillUnit(val));
+ self.curr_size += gap;
+ Ok(&mut self.buffer[..size])
+ }
+}
+
+/// A helper to check and get a mutable sub slice.
+fn get_mut<L: TryInto<usize>, R: TryInto<usize>>(
+ bytes: &mut [u8],
+ start: L,
+ end: R,
+) -> Result<&mut [u8], CommandError> {
+ bytes.get_mut(to_usize(start)?..to_usize(end)?).ok_or(ERR_IMAGE_SIZE.into())
+}
+
+/// A helper to check and get a sub slice.
+fn get<L: TryInto<usize>, R: TryInto<usize>>(
+ bytes: &[u8],
+ start: L,
+ end: R,
+) -> Result<&[u8], CommandError> {
+ bytes.get(to_usize(start)?..to_usize(end)?).ok_or(ERR_IMAGE_SIZE.into())
+}
+
+/// A helper to return a copy of a zerocopy object from bytes.
+fn copy_from<T: AsBytes + FromBytes + Default>(bytes: &[u8]) -> Result<T, CommandError> {
+ let mut res: T = Default::default();
+ res.as_bytes_mut().clone_from_slice(get(bytes, 0, size_of::<T>())?);
+ Ok(res)
+}
+
+/// Checks and converts an integer into usize.
+fn to_usize<T: TryInto<usize>>(val: T) -> Result<usize, CommandError> {
+ Ok(val.try_into().map_err(|_| ERR_ARITHMETIC_OVERFLOW)?)
+}
+
+/// Adds two usize convertible numbers and checks overflow.
+fn usize_add<L: TryInto<usize>, R: TryInto<usize>>(lhs: L, rhs: R) -> Result<usize, CommandError> {
+ Ok(to_usize(lhs)?.checked_add(to_usize(rhs)?).ok_or(ERR_ARITHMETIC_OVERFLOW)?)
+}
+
+/// Checks and converts an integer into u64
+fn to_u64<T: TryInto<u64>>(val: T) -> Result<u64, CommandError> {
+ Ok(val.try_into().map_err(|_| ERR_ARITHMETIC_OVERFLOW)?)
+}
+
+/// Adds two u64 convertible numbers and checks overflow.
+fn u64_add<L: TryInto<u64>, R: TryInto<u64>>(lhs: L, rhs: R) -> Result<u64, CommandError> {
+ Ok(to_u64(lhs)?.checked_add(to_u64(rhs)?).ok_or(ERR_ARITHMETIC_OVERFLOW)?)
+}
+
+/// Multiplies two u64 convertible numbers and checks overflow.
+fn u64_mul<L: TryInto<u64>, R: TryInto<u64>>(lhs: L, rhs: R) -> Result<u64, CommandError> {
+ Ok(to_u64(lhs)?.checked_mul(to_u64(rhs)?).ok_or(ERR_ARITHMETIC_OVERFLOW)?)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_sparse_write() {
+ let raw = include_bytes!("../../testdata/sparse_test_raw.bin");
+ let sparse = include_bytes!("../../testdata/sparse_test.bin");
+ // Gives a larger output buffer.
+ let mut out = vec![0u8; 2 * raw.len()];
+ assert_eq!(
+ write_sparse_image(&mut sparse.to_vec()[..], |off, data| {
+ out[off.try_into().unwrap()..][..data.len()].clone_from_slice(data);
+ Ok(())
+ })
+ .unwrap(),
+ raw.len().try_into().unwrap()
+ );
+ assert_eq!(out[..raw.len()].to_vec(), raw);
+ }
+
+ #[test]
+ fn test_sparse_write_non_default_block_size() {
+ let raw = include_bytes!("../../testdata/sparse_test_raw.bin");
+ let sparse = include_bytes!("../../testdata/sparse_test_blk1024.bin");
+ // Gives a larger output buffer.
+ let mut out = vec![0u8; 2 * raw.len()];
+ assert_eq!(
+ write_sparse_image(&mut sparse.to_vec()[..], |off, data| {
+ out[off.try_into().unwrap()..][..data.len()].clone_from_slice(data);
+ Ok(())
+ })
+ .unwrap(),
+ raw.len().try_into().unwrap()
+ );
+ assert_eq!(out[..raw.len()].to_vec(), raw);
+ }
+
+ /// A helper to copy a zerocopy object into a buffer
+ fn copy_to<T: AsBytes + FromBytes>(val: &T, bytes: &mut [u8]) {
+ bytes[..size_of::<T>()].clone_from_slice(val.as_bytes());
+ }
+
+ #[test]
+ fn test_sparse_invalid_magic() {
+ let mut sparse = include_bytes!("../../testdata/sparse_test.bin").to_vec();
+ let mut sparse_header: SparseHeader = copy_from(&sparse[..]).unwrap();
+ sparse_header.magic = 0;
+ copy_to(&sparse_header, &mut sparse[..]);
+ assert!(write_sparse_image(&mut sparse[..], |_, _| panic!()).is_err());
+ }
+
+ #[test]
+ fn test_sparse_invalid_major_version() {
+ let mut sparse = include_bytes!("../../testdata/sparse_test.bin").to_vec();
+ let mut sparse_header: SparseHeader = copy_from(&sparse[..]).unwrap();
+ sparse_header.major_version = SPARSE_HEADER_MAJOR_VER + 1;
+ copy_to(&sparse_header, &mut sparse[..]);
+ assert!(write_sparse_image(&mut sparse[..], |_, _| panic!()).is_err());
+ }
+
+ #[test]
+ fn test_sparse_invalid_minor_version() {
+ let mut sparse = include_bytes!("../../testdata/sparse_test.bin").to_vec();
+ let mut sparse_header: SparseHeader = copy_from(&sparse[..]).unwrap();
+ sparse_header.minor_version = SPARSE_HEADER_MINOR_VER + 1;
+ copy_to(&sparse_header, &mut sparse[..]);
+ assert!(write_sparse_image(&mut sparse[..], |_, _| panic!()).is_err());
+ }
+}
diff --git a/gbl/libgbl/testdata/gen_sparse_test_bin.py b/gbl/libgbl/testdata/gen_sparse_test_bin.py
new file mode 100644
index 0000000..8283a94
--- /dev/null
+++ b/gbl/libgbl/testdata/gen_sparse_test_bin.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generate test files for sparse image flash test"""
+
+import os
+import pathlib
+import subprocess
+
+SCRIPT_DIR = pathlib.Path(os.path.dirname(os.path.realpath(__file__)))
+
+# Writes bytes to a file at a given offset.
+def write_file(file, offset, data):
+ file.seek(offset, 0)
+ file.write(data)
+
+
+if __name__ == '__main__':
+ sz_kb = 1024
+ out_file_raw = SCRIPT_DIR / "sparse_test_raw.bin"
+ with open(out_file_raw, "wb") as f:
+ # 4k filled with 0x78563412
+ write_file(f, 0, b"\x12\x34\x56\x78" * 1024)
+ # 8k file hole (will become dont-care with the "-s" option)
+ # 12k raw data
+ write_file(f, 12 * sz_kb, os.urandom(12 * sz_kb))
+ # 8k filled with 0x78563412
+ write_file(f, 24 * sz_kb, b"\x12\x34\x56\x78" * 1024 * 2)
+ # 12k raw data
+ write_file(f, 32 * sz_kb, os.urandom(12 * sz_kb))
+ # 4k filled with 0x78563412
+ write_file(f, 44 * sz_kb, b"\x12\x34\x56\x78" * 1024)
+ # 8k filled with 0xEFCDAB90
+ write_file(f, 48 * sz_kb, b"\x90\xab\xcd\xef" * 1024 * 2)
+
+ subprocess.run(
+ ["img2simg", "-s", out_file_raw, SCRIPT_DIR / "sparse_test.bin"])
+ subprocess.run([
+ "img2simg",
+ "-s",
+ out_file_raw,
+ SCRIPT_DIR / "sparse_test_blk1024.bin",
+ "1024",
+ ])
diff --git a/gbl/libgbl/testdata/sparse_test.bin b/gbl/libgbl/testdata/sparse_test.bin
new file mode 100644
index 0000000..1721841
--- /dev/null
+++ b/gbl/libgbl/testdata/sparse_test.bin
Binary files differ
diff --git a/gbl/libgbl/testdata/sparse_test_blk1024.bin b/gbl/libgbl/testdata/sparse_test_blk1024.bin
new file mode 100644
index 0000000..bf79c68
--- /dev/null
+++ b/gbl/libgbl/testdata/sparse_test_blk1024.bin
Binary files differ
diff --git a/gbl/libgbl/testdata/sparse_test_raw.bin b/gbl/libgbl/testdata/sparse_test_raw.bin
new file mode 100644
index 0000000..1272534
--- /dev/null
+++ b/gbl/libgbl/testdata/sparse_test_raw.bin
Binary files differ