Merge "Add support for non-A/B OTA package in OTA_analysis."
This commit is contained in:
@@ -34,7 +34,7 @@
|
||||
<li>
|
||||
<strong> Incremental </strong>
|
||||
<!-- Check if the first partition is incremental or not -->
|
||||
<span v-if="payload.manifest.partitions[0].oldPartitionInfo">
|
||||
<span v-if="payload.preBuild">
|
||||
✅
|
||||
</span>
|
||||
<span v-else> ❌ </span>
|
||||
@@ -44,6 +44,13 @@
|
||||
<span v-if="payload.manifest.partialUpdate"> ✅ </span>
|
||||
<span v-else> ❌ </span>
|
||||
</li>
|
||||
<li>
|
||||
<strong> A/B update </strong>
|
||||
<span v-if="!payload.manifest.nonAB">
|
||||
✅
|
||||
</span>
|
||||
<span v-else> ❌ </span>
|
||||
</li>
|
||||
<li>
|
||||
<strong> VAB </strong>
|
||||
<span v-if="payload.manifest.dynamicPartitionMetadata.snapshotEnabled">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<ul>
|
||||
<h5> {{ mapType.get(operation.type) }} </h5>
|
||||
<h5> {{ mapType.getWithDefault(operation.type) }} </h5>
|
||||
<li v-if="operation.hasOwnProperty('dataOffset')">
|
||||
<strong> Data offset: </strong> {{ operation.dataOffset }}
|
||||
</li>
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<script>
|
||||
import { numBlocks, displayBlocks } from '../services/payload_composition.js'
|
||||
import { DefaultMap } from '../services/payload.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -33,7 +34,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
mapType: {
|
||||
type: Map,
|
||||
type: DefaultMap,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
<p v-if="partition.estimateCowSize">
|
||||
<strong> Estimate COW Size: </strong> {{ partition.estimateCowSize }} Bytes
|
||||
</p>
|
||||
<p v-else>
|
||||
<strong> Estimate COW Size: </strong> 0 Bytes
|
||||
</p>
|
||||
<div
|
||||
class="toggle"
|
||||
@click="toggle('showInfo')"
|
||||
@@ -30,7 +27,7 @@
|
||||
</strong>
|
||||
{{ partition.newPartitionInfo.size }} Bytes
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="partition.newPartitionInfo.hash">
|
||||
<strong>
|
||||
New Partition Hash:
|
||||
</strong>
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
md="6"
|
||||
>
|
||||
<v-btn
|
||||
:disabled="manifest.nonAB"
|
||||
block
|
||||
@click="updateChart('COWmerge')"
|
||||
>
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<h3>Metadata Signature</h3>
|
||||
<div
|
||||
v-if="payload.metadata_signature"
|
||||
v-if="payload.metadata_signature && !payload.manifest.nonAB"
|
||||
class="signature"
|
||||
>
|
||||
<h3>Metadata Signature</h3>
|
||||
<span style="white-space: pre-wrap">
|
||||
{{ octToHex(payload.metadata_signature.signatures[0].data) }}
|
||||
</span>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import * as zip from '@zip.js/zip.js/dist/zip-full.min.js'
|
||||
import { chromeos_update_engine as update_metadata_pb } from './update_metadata_pb.js'
|
||||
import { PayloadNonAB } from './payload_nonab.js'
|
||||
|
||||
const /** String */ _MAGIC = 'CrAU'
|
||||
const /** Number */ _VERSION_SIZE = 8
|
||||
@@ -134,8 +135,17 @@ export class Payload {
|
||||
}
|
||||
}
|
||||
if (!this.payload) {
|
||||
alert('Please select a legit OTA package')
|
||||
return
|
||||
try {
|
||||
// The temporary variable manifest has to be used here, to prevent the html page
|
||||
// being rendered before everything is read in properly
|
||||
let manifest = new PayloadNonAB(this.packedFile)
|
||||
await manifest.init()
|
||||
manifest.nonAB = true
|
||||
this.manifest = manifest
|
||||
} catch (error) {
|
||||
alert('Please select a legit OTA package')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,6 +218,7 @@ export class Payload {
|
||||
this.cursor += this.manifest_len
|
||||
this.manifest = update_metadata_pb.DeltaArchiveManifest
|
||||
.decode(new Uint8Array(buffer))
|
||||
this.manifest.nonAB = false
|
||||
}
|
||||
|
||||
async readSignature(/** @type {Blob} */buffer) {
|
||||
@@ -235,6 +246,17 @@ export class Payload {
|
||||
|
||||
}
|
||||
|
||||
export class DefaultMap extends Map {
|
||||
/** Reload the original get method. Return the original key value if
|
||||
* the key does not exist.
|
||||
* @param {Any} key
|
||||
*/
|
||||
getWithDefault(key) {
|
||||
if (!this.has(key)) return key
|
||||
return this.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export class OpType {
|
||||
/**
|
||||
* OpType.mapType create a map that could resolve the operation
|
||||
@@ -243,8 +265,8 @@ export class OpType {
|
||||
*/
|
||||
constructor() {
|
||||
let /** Array<{String: Number}>*/ types = update_metadata_pb.InstallOperation.Type
|
||||
this.mapType = new Map()
|
||||
for (let key in types) {
|
||||
this.mapType = new DefaultMap()
|
||||
for (let key of Object.keys(types)) {
|
||||
this.mapType.set(types[key], key)
|
||||
}
|
||||
}
|
||||
@@ -259,8 +281,8 @@ export class MergeOpType {
|
||||
constructor() {
|
||||
let /** Array<{String: Number}>*/ types =
|
||||
update_metadata_pb.CowMergeOperation.Type
|
||||
this.mapType = new Map()
|
||||
for (let key in types) {
|
||||
this.mapType = new DefaultMap()
|
||||
for (let key of Object.keys(types)) {
|
||||
this.mapType.set(types[key], key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export function operatedBlockStatistics(partitions) {
|
||||
let /** OpType */ opType = new OpType()
|
||||
for (let partition of partitions) {
|
||||
for (let operation of partition.operations) {
|
||||
let operationType = opType.mapType.get(operation.type)
|
||||
let operationType = opType.mapType.getWithDefault(operation.type)
|
||||
addNumberToMap(
|
||||
operatedBlocks,
|
||||
operationType,
|
||||
@@ -50,7 +50,7 @@ export function mergeOperationStatistics(partitions, blockSize) {
|
||||
let /** Number */ totalBlocks = 0
|
||||
for (let partition of partitions) {
|
||||
for (let operation of partition.mergeOperations) {
|
||||
let operationType = opType.mapType.get(operation.type)
|
||||
let operationType = opType.mapType.getWithDefault(operation.type)
|
||||
addNumberToMap(
|
||||
mergeOperations,
|
||||
operationType,
|
||||
@@ -79,7 +79,7 @@ export function operatedPayloadStatistics(partitions) {
|
||||
let /** OpType */ opType = new OpType()
|
||||
for (let partition of partitions) {
|
||||
for (let operation of partition.operations) {
|
||||
let operationType = opType.mapType.get(operation.type)
|
||||
let operationType = opType.mapType.getWithDefault(operation.type)
|
||||
addNumberToMap(
|
||||
operatedBlocks,
|
||||
operationType,
|
||||
|
||||
145
tools/ota_analysis/src/services/payload_nonab.js
Normal file
145
tools/ota_analysis/src/services/payload_nonab.js
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as zip from '@zip.js/zip.js/dist/zip-full.min.js'
|
||||
import { chromeos_update_engine } from './update_metadata_pb.js'
|
||||
|
||||
/**
|
||||
* Parse the non-A/B OTA package and return it as a DeltaArchiveManifest
|
||||
* @param {ZipReader} packedFile
|
||||
*/
|
||||
export class PayloadNonAB extends chromeos_update_engine.DeltaArchiveManifest{
|
||||
constructor(packedFile) {
|
||||
super()
|
||||
this.packedFile = packedFile
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.Blocksize = 4096
|
||||
this.partialUpdate = false
|
||||
this.dynamicPartitionMetadata =
|
||||
new chromeos_update_engine.DynamicPartitionMetadata(
|
||||
{ snapshotEnabled: false, vabcEnabled: false }
|
||||
)
|
||||
this.partitions = []
|
||||
|
||||
const /** RegExp*/ regexName = /[\w_]+(?=\.transfer.list)/g
|
||||
const /** Array<Entry> */ entries = await this.packedFile.getEntries()
|
||||
for (const entry of entries) {
|
||||
if (entry.filename.match(regexName)) {
|
||||
let newPartition = new chromeos_update_engine.PartitionUpdate(
|
||||
{partitionName: entry.filename.match(regexName)[0]}
|
||||
)
|
||||
newPartition.rawText = await entry.getData(new zip.TextWriter())
|
||||
await this.parseTransferList(newPartition)
|
||||
this.partitions.push(newPartition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async parseTransferList(partition) {
|
||||
let /** Array<String> */ lines = partition.rawText.split('\n')
|
||||
// First four line in header: version, total blocks,
|
||||
// number of stashed entries, maximum used memory for stash
|
||||
if (lines.length < 4) {
|
||||
throw "At least 4 lines in header should be provided."
|
||||
}
|
||||
partition.version = parseInt(lines[0])
|
||||
partition.totalBlocks = parseInt(lines[1])
|
||||
partition.entryStashed = parseInt(lines[2])
|
||||
partition.maxStashed = parseInt(lines[3])
|
||||
partition.newPartitionInfo = new chromeos_update_engine.PartitionInfo()
|
||||
partition.newPartitionInfo.hash = ''
|
||||
partition.newPartitionInfo.size = 'unknown'
|
||||
/**
|
||||
* The main body have 8 different ops:
|
||||
* zero [rangeset] : fill zeros
|
||||
* new [rangeset] : fill with new data from <partitionName.new.data>
|
||||
* erase [rangeset] : mark given blocks as empty
|
||||
* move <...>
|
||||
* bsdiff <patchstart> <patchlen> <...>
|
||||
* imgdiff <patchstart> <patchlen> <...> :
|
||||
* Read the source blocks and apply (not for move op) to the target blocks
|
||||
* stash <stash_id> <src_range> : load the given source range to memory
|
||||
* free <stash_id> : free the given <stash_id>
|
||||
* format:
|
||||
* [rangeset]: <# of pairs>, <pair A start>, <pair A end>, ...
|
||||
* <stash_id>: a hex number with length of 40
|
||||
*/
|
||||
partition.operations = new Array()
|
||||
let newDataSize = await this.sizeNewData(partition.partitionName)
|
||||
for (const line of lines.slice(4)) {
|
||||
let op = new chromeos_update_engine.InstallOperation()
|
||||
let elements = line.split(' ')
|
||||
op.type = elements[0]
|
||||
switch (op.type) {
|
||||
case 'zero':
|
||||
op.dstExtents = elements.slice(1).reduce(parseRange, [])
|
||||
break
|
||||
case 'new':
|
||||
// unlike an A/B OTA, the payload only exists in the payload.bin,
|
||||
// in an non-A/B OTA, the payload exists in both .new.data and .patch.data.
|
||||
// The new ops do not have any information about data length.
|
||||
// what we do here is: read in the size of .new.data, assume the first new
|
||||
// op have the data length of the size of .new.data.
|
||||
op.dataLength = newDataSize
|
||||
newDataSize = 0
|
||||
op.dstExtents = elements.slice(1).reduce(parseRange, [])
|
||||
break
|
||||
case 'erase':
|
||||
op.dstExtents = elements.slice(1).reduce(parseRange, [])
|
||||
break
|
||||
case 'move':
|
||||
break
|
||||
case 'bsdiff':
|
||||
op.dataOffset = parseInt(elements[1])
|
||||
op.dataLength = parseInt(elements[2])
|
||||
break
|
||||
case 'imgdiff':
|
||||
op.dataOffset = parseInt(elements[1])
|
||||
op.dataLength = parseInt(elements[2])
|
||||
break
|
||||
case 'stash':
|
||||
break
|
||||
case 'free':
|
||||
break
|
||||
}
|
||||
partition.operations.push(op)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the size of <partitionName>.new.data.*
|
||||
* @param {String} partitionName
|
||||
* @return {Number}
|
||||
*/
|
||||
async sizeNewData(partitionName) {
|
||||
const /** Array<Entry> */ entries = await this.packedFile.getEntries()
|
||||
const /** RegExp */ regexName = new RegExp(partitionName + '.new.dat.*')
|
||||
for (const entry of entries) {
|
||||
if (entry.filename.match(regexName)) {
|
||||
return entry.uncompressedSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the range string and return it as an array of extents
|
||||
* @param {extents} Array<extents>
|
||||
* @param {String} rangeset
|
||||
* @return Array<extents>
|
||||
*/
|
||||
function parseRange(extents, rangeset) {
|
||||
const regexRange = new RegExp('[\d,]+')
|
||||
if (rangeset.match(regexRange)) {
|
||||
let elements = rangeset.split(',')
|
||||
for (let i=1; i<elements.length; i=i+2) {
|
||||
let extent = new Object(
|
||||
{
|
||||
startBlock: parseInt(elements[i]),
|
||||
numBlocks: parseInt(elements[i+1]) - parseInt(elements[i])
|
||||
}
|
||||
)
|
||||
extents.push(extent)
|
||||
}
|
||||
}
|
||||
return extents
|
||||
}
|
||||
Reference in New Issue
Block a user