Android Dynamic Partitions metadata: JavaScript parsing library

The metadata stored by Android at the beginning of a "super" partition, which is what it calls a disk partition that holds one or more Dynamic Partitions. Dynamic Partitions do more or less the same thing that LVM does on Linux, allowing Android to map ranges of non-contiguous extents to a single logical device. This metadata holds that mapping.

Application

Android

File extension

img

KS implementation details

License: CC0-1.0
Minimal Kaitai Struct required: 0.9

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

Usage

Runtime library

All parsing code for JavaScript generated by Kaitai Struct depends on the JavaScript runtime library. You have to install it before you can parse data.

The JavaScript runtime library is available at npm:

npm install kaitai-struct

Code

See the usage examples in the JavaScript notes.

Parse structure from an ArrayBuffer:

var arrayBuffer = ...;
var data = new AndroidSuper(new KaitaiStream(arrayBuffer));

After that, one can get various attributes from the structure by accessing fields or properties like:

data.root // => get root

JavaScript source code to parse Android Dynamic Partitions metadata

AndroidSuper.js

// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['kaitai-struct/KaitaiStream'], factory);
  } else if (typeof module === 'object' && module.exports) {
    module.exports = factory(require('kaitai-struct/KaitaiStream'));
  } else {
    root.AndroidSuper = factory(root.KaitaiStream);
  }
}(typeof self !== 'undefined' ? self : this, function (KaitaiStream) {
/**
 * The metadata stored by Android at the beginning of a "super" partition, which
 * is what it calls a disk partition that holds one or more Dynamic Partitions.
 * Dynamic Partitions do more or less the same thing that LVM does on Linux,
 * allowing Android to map ranges of non-contiguous extents to a single logical
 * device. This metadata holds that mapping.
 * @see {@link https://source.android.com/docs/core/ota/dynamic_partitions|Source}
 * @see {@link https://android.googlesource.com/platform/system/core/+/refs/tags/android-11.0.0_r8/fs_mgr/liblp/include/liblp/metadata_format.h|Source}
 */

var AndroidSuper = (function() {
  function AndroidSuper(_io, _parent, _root) {
    this._io = _io;
    this._parent = _parent;
    this._root = _root || this;

    this._read();
  }
  AndroidSuper.prototype._read = function() {
  }

  var Root = AndroidSuper.Root = (function() {
    function Root(_io, _parent, _root) {
      this._io = _io;
      this._parent = _parent;
      this._root = _root || this;

      this._read();
    }
    Root.prototype._read = function() {
      this._raw_primaryGeometry = this._io.readBytes(4096);
      var _io__raw_primaryGeometry = new KaitaiStream(this._raw_primaryGeometry);
      this.primaryGeometry = new Geometry(_io__raw_primaryGeometry, this, this._root);
      this._raw_backupGeometry = this._io.readBytes(4096);
      var _io__raw_backupGeometry = new KaitaiStream(this._raw_backupGeometry);
      this.backupGeometry = new Geometry(_io__raw_backupGeometry, this, this._root);
      this._raw_primaryMetadata = [];
      this.primaryMetadata = [];
      for (var i = 0; i < this.primaryGeometry.metadataSlotCount; i++) {
        this._raw_primaryMetadata.push(this._io.readBytes(this.primaryGeometry.metadataMaxSize));
        var _io__raw_primaryMetadata = new KaitaiStream(this._raw_primaryMetadata[i]);
        this.primaryMetadata.push(new Metadata(_io__raw_primaryMetadata, this, this._root));
      }
      this._raw_backupMetadata = [];
      this.backupMetadata = [];
      for (var i = 0; i < this.primaryGeometry.metadataSlotCount; i++) {
        this._raw_backupMetadata.push(this._io.readBytes(this.primaryGeometry.metadataMaxSize));
        var _io__raw_backupMetadata = new KaitaiStream(this._raw_backupMetadata[i]);
        this.backupMetadata.push(new Metadata(_io__raw_backupMetadata, this, this._root));
      }
    }

    return Root;
  })();

  var Geometry = AndroidSuper.Geometry = (function() {
    function Geometry(_io, _parent, _root) {
      this._io = _io;
      this._parent = _parent;
      this._root = _root || this;

      this._read();
    }
    Geometry.prototype._read = function() {
      this.magic = this._io.readBytes(4);
      if (!((KaitaiStream.byteArrayCompare(this.magic, [103, 68, 108, 97]) == 0))) {
        throw new KaitaiStream.ValidationNotEqualError([103, 68, 108, 97], this.magic, this._io, "/types/geometry/seq/0");
      }
      this.structSize = this._io.readU4le();
      this.checksum = this._io.readBytes(32);
      this.metadataMaxSize = this._io.readU4le();
      this.metadataSlotCount = this._io.readU4le();
      this.logicalBlockSize = this._io.readU4le();
    }

    /**
     * SHA-256 hash of struct_size bytes from beginning of geometry,
     * calculated as if checksum were zeroed out
     */

    return Geometry;
  })();

  var Metadata = AndroidSuper.Metadata = (function() {
    Metadata.TableKind = Object.freeze({
      PARTITIONS: 0,
      EXTENTS: 1,
      GROUPS: 2,
      BLOCK_DEVICES: 3,

      0: "PARTITIONS",
      1: "EXTENTS",
      2: "GROUPS",
      3: "BLOCK_DEVICES",
    });

    function Metadata(_io, _parent, _root) {
      this._io = _io;
      this._parent = _parent;
      this._root = _root || this;

      this._read();
    }
    Metadata.prototype._read = function() {
      this.magic = this._io.readBytes(4);
      if (!((KaitaiStream.byteArrayCompare(this.magic, [48, 80, 76, 65]) == 0))) {
        throw new KaitaiStream.ValidationNotEqualError([48, 80, 76, 65], this.magic, this._io, "/types/metadata/seq/0");
      }
      this.majorVersion = this._io.readU2le();
      this.minorVersion = this._io.readU2le();
      this.headerSize = this._io.readU4le();
      this.headerChecksum = this._io.readBytes(32);
      this.tablesSize = this._io.readU4le();
      this.tablesChecksum = this._io.readBytes(32);
      this.partitions = new TableDescriptor(this._io, this, this._root, AndroidSuper.Metadata.TableKind.PARTITIONS);
      this.extents = new TableDescriptor(this._io, this, this._root, AndroidSuper.Metadata.TableKind.EXTENTS);
      this.groups = new TableDescriptor(this._io, this, this._root, AndroidSuper.Metadata.TableKind.GROUPS);
      this.blockDevices = new TableDescriptor(this._io, this, this._root, AndroidSuper.Metadata.TableKind.BLOCK_DEVICES);
    }

    var BlockDevice = Metadata.BlockDevice = (function() {
      function BlockDevice(_io, _parent, _root) {
        this._io = _io;
        this._parent = _parent;
        this._root = _root || this;

        this._read();
      }
      BlockDevice.prototype._read = function() {
        this.firstLogicalSector = this._io.readU8le();
        this.alignment = this._io.readU4le();
        this.alignmentOffset = this._io.readU4le();
        this.size = this._io.readU8le();
        this.partitionName = KaitaiStream.bytesToStr(KaitaiStream.bytesTerminate(this._io.readBytes(36), 0, false), "UTF-8");
        this.flagSlotSuffixed = this._io.readBitsIntLe(1) != 0;
        this.flagsReserved = this._io.readBitsIntLe(31);
      }

      return BlockDevice;
    })();

    var Extent = Metadata.Extent = (function() {
      Extent.TargetType = Object.freeze({
        LINEAR: 0,
        ZERO: 1,

        0: "LINEAR",
        1: "ZERO",
      });

      function Extent(_io, _parent, _root) {
        this._io = _io;
        this._parent = _parent;
        this._root = _root || this;

        this._read();
      }
      Extent.prototype._read = function() {
        this.numSectors = this._io.readU8le();
        this.targetType = this._io.readU4le();
        this.targetData = this._io.readU8le();
        this.targetSource = this._io.readU4le();
      }

      return Extent;
    })();

    var TableDescriptor = Metadata.TableDescriptor = (function() {
      function TableDescriptor(_io, _parent, _root, kind) {
        this._io = _io;
        this._parent = _parent;
        this._root = _root || this;
        this.kind = kind;

        this._read();
      }
      TableDescriptor.prototype._read = function() {
        this.offset = this._io.readU4le();
        this.numEntries = this._io.readU4le();
        this.entrySize = this._io.readU4le();
      }
      Object.defineProperty(TableDescriptor.prototype, 'table', {
        get: function() {
          if (this._m_table !== undefined)
            return this._m_table;
          var _pos = this._io.pos;
          this._io.seek((this._parent.headerSize + this.offset));
          this._raw__m_table = [];
          this._m_table = [];
          for (var i = 0; i < this.numEntries; i++) {
            switch (this.kind) {
            case AndroidSuper.Metadata.TableKind.PARTITIONS:
              this._raw__m_table.push(this._io.readBytes(this.entrySize));
              var _io__raw__m_table = new KaitaiStream(this._raw__m_table[i]);
              this._m_table.push(new Partition(_io__raw__m_table, this, this._root));
              break;
            case AndroidSuper.Metadata.TableKind.EXTENTS:
              this._raw__m_table.push(this._io.readBytes(this.entrySize));
              var _io__raw__m_table = new KaitaiStream(this._raw__m_table[i]);
              this._m_table.push(new Extent(_io__raw__m_table, this, this._root));
              break;
            case AndroidSuper.Metadata.TableKind.GROUPS:
              this._raw__m_table.push(this._io.readBytes(this.entrySize));
              var _io__raw__m_table = new KaitaiStream(this._raw__m_table[i]);
              this._m_table.push(new Group(_io__raw__m_table, this, this._root));
              break;
            case AndroidSuper.Metadata.TableKind.BLOCK_DEVICES:
              this._raw__m_table.push(this._io.readBytes(this.entrySize));
              var _io__raw__m_table = new KaitaiStream(this._raw__m_table[i]);
              this._m_table.push(new BlockDevice(_io__raw__m_table, this, this._root));
              break;
            default:
              this._m_table.push(this._io.readBytes(this.entrySize));
              break;
            }
          }
          this._io.seek(_pos);
          return this._m_table;
        }
      });

      return TableDescriptor;
    })();

    var Partition = Metadata.Partition = (function() {
      function Partition(_io, _parent, _root) {
        this._io = _io;
        this._parent = _parent;
        this._root = _root || this;

        this._read();
      }
      Partition.prototype._read = function() {
        this.name = KaitaiStream.bytesToStr(KaitaiStream.bytesTerminate(this._io.readBytes(36), 0, false), "UTF-8");
        this.attrReadonly = this._io.readBitsIntLe(1) != 0;
        this.attrSlotSuffixed = this._io.readBitsIntLe(1) != 0;
        this.attrUpdated = this._io.readBitsIntLe(1) != 0;
        this.attrDisabled = this._io.readBitsIntLe(1) != 0;
        this.attrsReserved = this._io.readBitsIntLe(28);
        this._io.alignToByte();
        this.firstExtentIndex = this._io.readU4le();
        this.numExtents = this._io.readU4le();
        this.groupIndex = this._io.readU4le();
      }

      return Partition;
    })();

    var Group = Metadata.Group = (function() {
      function Group(_io, _parent, _root) {
        this._io = _io;
        this._parent = _parent;
        this._root = _root || this;

        this._read();
      }
      Group.prototype._read = function() {
        this.name = KaitaiStream.bytesToStr(KaitaiStream.bytesTerminate(this._io.readBytes(36), 0, false), "UTF-8");
        this.flagSlotSuffixed = this._io.readBitsIntLe(1) != 0;
        this.flagsReserved = this._io.readBitsIntLe(31);
        this._io.alignToByte();
        this.maximumSize = this._io.readU8le();
      }

      return Group;
    })();

    /**
     * SHA-256 hash of header_size bytes from beginning of metadata,
     * calculated as if header_checksum were zeroed out
     */

    /**
     * SHA-256 hash of tables_size bytes from end of header
     */

    return Metadata;
  })();
  Object.defineProperty(AndroidSuper.prototype, 'root', {
    get: function() {
      if (this._m_root !== undefined)
        return this._m_root;
      var _pos = this._io.pos;
      this._io.seek(4096);
      this._m_root = new Root(this._io, this, this._root);
      this._io.seek(_pos);
      return this._m_root;
    }
  });

  return AndroidSuper;
})();
return AndroidSuper;
}));