Android sparse image: Python (read-write) parsing library

The Android sparse format is a format to more efficiently store files for for example firmware updates to save on bandwidth. Files in sparse format first have to be converted back to their original format.

A tool to create images for testing can be found in the Android source code tree:

https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse - img2simg.c

Note: this is not the same as the Android sparse data image format.

File extension

img

KS implementation details

License: CC0-1.0
Minimal Kaitai Struct required: 0.9

This page hosts a formal specification of Android sparse image using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Python (read-write) source code to parse Android sparse image

android_sparse.py

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild
# type: ignore

import kaitaistruct
from kaitaistruct import ReadWriteKaitaiStruct, KaitaiStream, BytesIO
from enum import IntEnum


if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 11):
    raise Exception("Incompatible Kaitai Struct Python API: 0.11 or later is required, but you have %s" % (kaitaistruct.__version__))

class AndroidSparse(ReadWriteKaitaiStruct):
    """The Android sparse format is a format to more efficiently store files
    for for example firmware updates to save on bandwidth. Files in sparse
    format first have to be converted back to their original format.
    
    A tool to create images for testing can be found in the Android source code tree:
    
    <https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse> - `img2simg.c`
    
    Note: this is not the same as the Android sparse data image format.
    
    .. seealso::
       Source - https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse/sparse_format.h
    
    
    .. seealso::
       Source - https://web.archive.org/web/20220322054458/https://source.android.com/devices/bootloader/images#sparse-image-format
    """

    class ChunkTypes(IntEnum):
        raw = 51905
        fill = 51906
        dont_care = 51907
        crc32 = 51908
    def __init__(self, _io=None, _parent=None, _root=None):
        super(AndroidSparse, self).__init__(_io)
        self._parent = _parent
        self._root = _root or self

    def _read(self):
        self.header_prefix = AndroidSparse.FileHeaderPrefix(self._io, self, self._root)
        self.header_prefix._read()
        self._raw_header = self._io.read_bytes(self.header_prefix.len_header - 10)
        _io__raw_header = KaitaiStream(BytesIO(self._raw_header))
        self.header = AndroidSparse.FileHeader(_io__raw_header, self, self._root)
        self.header._read()
        self.chunks = []
        for i in range(self.header.num_chunks):
            _t_chunks = AndroidSparse.Chunk(self._io, self, self._root)
            try:
                _t_chunks._read()
            finally:
                self.chunks.append(_t_chunks)

        self._dirty = False


    def _fetch_instances(self):
        pass
        self.header_prefix._fetch_instances()
        self.header._fetch_instances()
        for i in range(len(self.chunks)):
            pass
            self.chunks[i]._fetch_instances()



    def _write__seq(self, io=None):
        super(AndroidSparse, self)._write__seq(io)
        self.header_prefix._write__seq(self._io)
        _io__raw_header = KaitaiStream(BytesIO(bytearray(self.header_prefix.len_header - 10)))
        self._io.add_child_stream(_io__raw_header)
        _pos2 = self._io.pos()
        self._io.seek(self._io.pos() + (self.header_prefix.len_header - 10))
        def handler(parent, _io__raw_header=_io__raw_header):
            self._raw_header = _io__raw_header.to_byte_array()
            if len(self._raw_header) != self.header_prefix.len_header - 10:
                raise kaitaistruct.ConsistencyError(u"raw(header)", self.header_prefix.len_header - 10, len(self._raw_header))
            parent.write_bytes(self._raw_header)
        _io__raw_header.write_back_handler = KaitaiStream.WriteBackHandler(_pos2, handler)
        self.header._write__seq(_io__raw_header)
        for i in range(len(self.chunks)):
            pass
            self.chunks[i]._write__seq(self._io)



    def _check(self):
        if self.header_prefix._root != self._root:
            raise kaitaistruct.ConsistencyError(u"header_prefix", self._root, self.header_prefix._root)
        if self.header_prefix._parent != self:
            raise kaitaistruct.ConsistencyError(u"header_prefix", self, self.header_prefix._parent)
        if self.header._root != self._root:
            raise kaitaistruct.ConsistencyError(u"header", self._root, self.header._root)
        if self.header._parent != self:
            raise kaitaistruct.ConsistencyError(u"header", self, self.header._parent)
        if len(self.chunks) != self.header.num_chunks:
            raise kaitaistruct.ConsistencyError(u"chunks", self.header.num_chunks, len(self.chunks))
        for i in range(len(self.chunks)):
            pass
            if self.chunks[i]._root != self._root:
                raise kaitaistruct.ConsistencyError(u"chunks", self._root, self.chunks[i]._root)
            if self.chunks[i]._parent != self:
                raise kaitaistruct.ConsistencyError(u"chunks", self, self.chunks[i]._parent)

        self._dirty = False

    class Chunk(ReadWriteKaitaiStruct):
        def __init__(self, _io=None, _parent=None, _root=None):
            super(AndroidSparse.Chunk, self).__init__(_io)
            self._parent = _parent
            self._root = _root

        def _read(self):
            self._raw_header = self._io.read_bytes(self._root.header.len_chunk_header)
            _io__raw_header = KaitaiStream(BytesIO(self._raw_header))
            self.header = AndroidSparse.Chunk.ChunkHeader(_io__raw_header, self, self._root)
            self.header._read()
            _on = self.header.chunk_type
            if _on == AndroidSparse.ChunkTypes.crc32:
                pass
                self.body = self._io.read_u4le()
            else:
                pass
                self.body = self._io.read_bytes(self.header.len_body)
            self._dirty = False


        def _fetch_instances(self):
            pass
            self.header._fetch_instances()
            _on = self.header.chunk_type
            if _on == AndroidSparse.ChunkTypes.crc32:
                pass
            else:
                pass


        def _write__seq(self, io=None):
            super(AndroidSparse.Chunk, self)._write__seq(io)
            _io__raw_header = KaitaiStream(BytesIO(bytearray(self._root.header.len_chunk_header)))
            self._io.add_child_stream(_io__raw_header)
            _pos2 = self._io.pos()
            self._io.seek(self._io.pos() + (self._root.header.len_chunk_header))
            def handler(parent, _io__raw_header=_io__raw_header):
                self._raw_header = _io__raw_header.to_byte_array()
                if len(self._raw_header) != self._root.header.len_chunk_header:
                    raise kaitaistruct.ConsistencyError(u"raw(header)", self._root.header.len_chunk_header, len(self._raw_header))
                parent.write_bytes(self._raw_header)
            _io__raw_header.write_back_handler = KaitaiStream.WriteBackHandler(_pos2, handler)
            self.header._write__seq(_io__raw_header)
            _on = self.header.chunk_type
            if _on == AndroidSparse.ChunkTypes.crc32:
                pass
                self._io.write_u4le(self.body)
            else:
                pass
                self._io.write_bytes(self.body)


        def _check(self):
            if self.header._root != self._root:
                raise kaitaistruct.ConsistencyError(u"header", self._root, self.header._root)
            if self.header._parent != self:
                raise kaitaistruct.ConsistencyError(u"header", self, self.header._parent)
            _on = self.header.chunk_type
            if _on == AndroidSparse.ChunkTypes.crc32:
                pass
            else:
                pass
                if len(self.body) != self.header.len_body:
                    raise kaitaistruct.ConsistencyError(u"body", self.header.len_body, len(self.body))
            self._dirty = False

        class ChunkHeader(ReadWriteKaitaiStruct):
            def __init__(self, _io=None, _parent=None, _root=None):
                super(AndroidSparse.Chunk.ChunkHeader, self).__init__(_io)
                self._parent = _parent
                self._root = _root

            def _read(self):
                self.chunk_type = KaitaiStream.resolve_enum(AndroidSparse.ChunkTypes, self._io.read_u2le())
                self.reserved1 = self._io.read_u2le()
                self.num_body_blocks = self._io.read_u4le()
                self.len_chunk = self._io.read_u4le()
                if not self.len_chunk == (self._root.header.len_chunk_header + self.len_body_expected if self.len_body_expected != -1 else self.len_chunk):
                    raise kaitaistruct.ValidationNotEqualError((self._root.header.len_chunk_header + self.len_body_expected if self.len_body_expected != -1 else self.len_chunk), self.len_chunk, self._io, u"/types/chunk/types/chunk_header/seq/3")
                self._dirty = False


            def _fetch_instances(self):
                pass


            def _write__seq(self, io=None):
                super(AndroidSparse.Chunk.ChunkHeader, self)._write__seq(io)
                self._io.write_u2le(int(self.chunk_type))
                self._io.write_u2le(self.reserved1)
                self._io.write_u4le(self.num_body_blocks)
                self._io.write_u4le(self.len_chunk)


            def _check(self):
                if not self.len_chunk == (self._root.header.len_chunk_header + self.len_body_expected if self.len_body_expected != -1 else self.len_chunk):
                    raise kaitaistruct.ValidationNotEqualError((self._root.header.len_chunk_header + self.len_body_expected if self.len_body_expected != -1 else self.len_chunk), self.len_chunk, None, u"/types/chunk/types/chunk_header/seq/3")
                self._dirty = False

            @property
            def len_body(self):
                if hasattr(self, '_m_len_body'):
                    return self._m_len_body

                self._m_len_body = self.len_chunk - self._root.header.len_chunk_header
                return getattr(self, '_m_len_body', None)

            def _invalidate_len_body(self):
                del self._m_len_body
            @property
            def len_body_expected(self):
                """
                .. seealso::
                   Source - https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse/sparse_read.cpp#184
                
                
                .. seealso::
                   Source - https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse/sparse_read.cpp#215
                
                
                .. seealso::
                   Source - https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse/sparse_read.cpp#249
                
                
                .. seealso::
                   Source - https://android.googlesource.com/platform/system/core/+/e8d02c50d7/libsparse/sparse_read.cpp#270
                """
                if hasattr(self, '_m_len_body_expected'):
                    return self._m_len_body_expected

                self._m_len_body_expected = (self._root.header.block_size * self.num_body_blocks if self.chunk_type == AndroidSparse.ChunkTypes.raw else (4 if self.chunk_type == AndroidSparse.ChunkTypes.fill else (0 if self.chunk_type == AndroidSparse.ChunkTypes.dont_care else (4 if self.chunk_type == AndroidSparse.ChunkTypes.crc32 else -1))))
                return getattr(self, '_m_len_body_expected', None)

            def _invalidate_len_body_expected(self):
                del self._m_len_body_expected


    class FileHeader(ReadWriteKaitaiStruct):
        def __init__(self, _io=None, _parent=None, _root=None):
            super(AndroidSparse.FileHeader, self).__init__(_io)
            self._parent = _parent
            self._root = _root

        def _read(self):
            self.len_chunk_header = self._io.read_u2le()
            self.block_size = self._io.read_u4le()
            _ = self.block_size
            if not _ % 4 == 0:
                raise kaitaistruct.ValidationExprError(self.block_size, self._io, u"/types/file_header/seq/1")
            self.num_blocks = self._io.read_u4le()
            self.num_chunks = self._io.read_u4le()
            self.checksum = self._io.read_u4le()
            self._dirty = False


        def _fetch_instances(self):
            pass


        def _write__seq(self, io=None):
            super(AndroidSparse.FileHeader, self)._write__seq(io)
            self._io.write_u2le(self.len_chunk_header)
            self._io.write_u4le(self.block_size)
            self._io.write_u4le(self.num_blocks)
            self._io.write_u4le(self.num_chunks)
            self._io.write_u4le(self.checksum)


        def _check(self):
            _ = self.block_size
            if not _ % 4 == 0:
                raise kaitaistruct.ValidationExprError(self.block_size, None, u"/types/file_header/seq/1")
            self._dirty = False

        @property
        def len_header(self):
            """size of file header, should be 28."""
            if hasattr(self, '_m_len_header'):
                return self._m_len_header

            self._m_len_header = self._root.header_prefix.len_header
            return getattr(self, '_m_len_header', None)

        def _invalidate_len_header(self):
            del self._m_len_header
        @property
        def version(self):
            if hasattr(self, '_m_version'):
                return self._m_version

            self._m_version = self._root.header_prefix.version
            return getattr(self, '_m_version', None)

        def _invalidate_version(self):
            del self._m_version

    class FileHeaderPrefix(ReadWriteKaitaiStruct):
        def __init__(self, _io=None, _parent=None, _root=None):
            super(AndroidSparse.FileHeaderPrefix, self).__init__(_io)
            self._parent = _parent
            self._root = _root

        def _read(self):
            self.magic = self._io.read_bytes(4)
            if not self.magic == b"\x3A\xFF\x26\xED":
                raise kaitaistruct.ValidationNotEqualError(b"\x3A\xFF\x26\xED", self.magic, self._io, u"/types/file_header_prefix/seq/0")
            self.version = AndroidSparse.Version(self._io, self, self._root)
            self.version._read()
            self.len_header = self._io.read_u2le()
            self._dirty = False


        def _fetch_instances(self):
            pass
            self.version._fetch_instances()


        def _write__seq(self, io=None):
            super(AndroidSparse.FileHeaderPrefix, self)._write__seq(io)
            self._io.write_bytes(self.magic)
            self.version._write__seq(self._io)
            self._io.write_u2le(self.len_header)


        def _check(self):
            if len(self.magic) != 4:
                raise kaitaistruct.ConsistencyError(u"magic", 4, len(self.magic))
            if not self.magic == b"\x3A\xFF\x26\xED":
                raise kaitaistruct.ValidationNotEqualError(b"\x3A\xFF\x26\xED", self.magic, None, u"/types/file_header_prefix/seq/0")
            if self.version._root != self._root:
                raise kaitaistruct.ConsistencyError(u"version", self._root, self.version._root)
            if self.version._parent != self:
                raise kaitaistruct.ConsistencyError(u"version", self, self.version._parent)
            self._dirty = False


    class Version(ReadWriteKaitaiStruct):
        def __init__(self, _io=None, _parent=None, _root=None):
            super(AndroidSparse.Version, self).__init__(_io)
            self._parent = _parent
            self._root = _root

        def _read(self):
            self.major = self._io.read_u2le()
            if not self.major == 1:
                raise kaitaistruct.ValidationNotEqualError(1, self.major, self._io, u"/types/version/seq/0")
            self.minor = self._io.read_u2le()
            self._dirty = False


        def _fetch_instances(self):
            pass


        def _write__seq(self, io=None):
            super(AndroidSparse.Version, self)._write__seq(io)
            self._io.write_u2le(self.major)
            self._io.write_u2le(self.minor)


        def _check(self):
            if not self.major == 1:
                raise kaitaistruct.ValidationNotEqualError(1, self.major, None, u"/types/version/seq/0")
            self._dirty = False