Skip to content

Instantly share code, notes, and snippets.

@CrackerCat
Forked from snake-4/fast_simg2img.py
Created April 7, 2024 08:38
Show Gist options
  • Select an option

  • Save CrackerCat/729d936ddb1e32adf5711aa9a9e2846c to your computer and use it in GitHub Desktop.

Select an option

Save CrackerCat/729d936ddb1e32adf5711aa9a9e2846c to your computer and use it in GitHub Desktop.
Fast Android Sparse Image unpacker
#!/usr/bin/env python3
import sys
import struct
SPARSE_HEADER_SIZE = 28
SPARSE_HEADER_MAGIC = 0xED26FF3A
SPARSE_HEADER_MAJOR_VER = 1
SPARSE_CHUNK_SIZE = 12
SPARSE_CHUNK_TYPE_RAW = 0xCAC1
SPARSE_CHUNK_TYPE_FILL = 0xCAC2
SPARSE_CHUNK_TYPE_DONT_CARE = 0xCAC3
SPARSE_CHUNK_TYPE_CRC32 = 0xCAC4
class SparseHeader:
def __init__(self, buf):
(
self.magic,
self.major_version,
self.minor_version,
self.file_hdr_sz,
self.chunk_hdr_sz,
self.blk_sz,
self.total_blks,
self.total_chunks,
self.image_checksum,
) = struct.unpack("<IHHHHIIII", buf)
class SparseChunkHeader:
def __init__(self, buf):
(self.chunk_type, _, self.chunk_sz, self.total_sz) = struct.unpack("<HHII", buf)
# Not perfect but it should suffice. https://stackoverflow.com/a/1094933
def sizeof_fmt(num, suffix="B"):
for unit in ("", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"):
if abs(num) < 1024.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1024.0
return f"{num:.1f}Yi{suffix}"
def stream_sparse_zero(strm, size):
strm.seek(size - 1, 1)
strm.write(b"\x00")
# Default chunk_size is 4MiB
def stream_read_chunked(strm, size, chunk_size=0x400000):
while size > 0:
read_size = min(chunk_size, size)
data = strm.read(read_size)
if not data:
break
yield data
size -= len(data)
def main(argv):
in_path = argv[1]
out_path = in_path + ".raw"
if len(argv) > 2:
out_path = argv[2]
with open(in_path, "rb") as in_file:
file_header = SparseHeader(in_file.read(SPARSE_HEADER_SIZE))
if file_header.magic != SPARSE_HEADER_MAGIC:
raise Exception("Not an Android sparse image")
if file_header.major_version > SPARSE_HEADER_MAJOR_VER:
raise Exception("File major version mismatch")
if file_header.blk_sz % 4 != 0:
raise Exception("Block size is not a multiple of 4")
print(f"Processing {file_header.total_chunks} chunks...")
with open(out_path, "wb") as out_file:
for _ in range(file_header.total_chunks):
chunk_header = SparseChunkHeader(in_file.read(SPARSE_CHUNK_SIZE))
chunk_size_in_bytes = chunk_header.chunk_sz * file_header.blk_sz
pretty_size = sizeof_fmt(chunk_size_in_bytes)
if chunk_header.chunk_type == SPARSE_CHUNK_TYPE_RAW:
print(f"[Chunk] raw | size={pretty_size}")
for data_chunk in stream_read_chunked(in_file, chunk_size_in_bytes):
out_file.write(data_chunk)
elif chunk_header.chunk_type == SPARSE_CHUNK_TYPE_FILL:
fill_value = in_file.read(4)
print(f"[Chunk] fill | value={fill_value} | size={pretty_size}")
if fill_value == b"\x00\x00\x00\x00":
stream_sparse_zero(out_file, chunk_size_in_bytes)
else:
for _ in range(chunk_size_in_bytes // 4):
out_file.write(fill_value)
elif chunk_header.chunk_type == SPARSE_CHUNK_TYPE_DONT_CARE:
print(f"[Chunk] dont_care | size={pretty_size}")
stream_sparse_zero(out_file, chunk_size_in_bytes)
elif chunk_header.chunk_type == SPARSE_CHUNK_TYPE_CRC32:
crc32_value = int.from_bytes(in_file.read(4), "little")
print(f"[Chunk] crc32 | value=0x{crc32_value:08X}")
else:
raise Exception("Invalid chunk type")
print(f'Wrote result to "{out_path}"')
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} input_file [output_file]")
else:
try:
main(sys.argv)
except (OSError, IOError) as e:
print(f"[IO Error] {e.strerror}: {e.filename}")
sys.exit(1)
except Exception as e:
print(f"[Error] {e}")
sys.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment