Merge "Add support for analysis of OTA package."
This commit is contained in:
@@ -11,7 +11,11 @@
|
||||
"plugin:vue/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"indent": ["error", 2],
|
||||
"vue/no-multiple-template-root": 0
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
"vue/no-multiple-template-root": 0,
|
||||
"vue/attribute-hyphenation": 0
|
||||
}
|
||||
}
|
||||
27
tools/otagui/package-lock.json
generated
27
tools/otagui/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "OTA_GUI",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "^2.3.6",
|
||||
"core-js": "^3.6.5",
|
||||
"echarts": "^5.1.2",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
@@ -2944,6 +2945,11 @@
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.3.6.tgz",
|
||||
"integrity": "sha512-VQE2MI7YChMmdeBN9CGuktE4Mws+gUGAjWGUwzoIsT/gNmI+7BK+qbArsC5RO/NXYKJ1pj2vzNSZCyUFhPdG1g=="
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -2995,14 +3001,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
|
||||
"integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-errors": {
|
||||
@@ -18115,6 +18125,11 @@
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.3.6.tgz",
|
||||
"integrity": "sha512-VQE2MI7YChMmdeBN9CGuktE4Mws+gUGAjWGUwzoIsT/gNmI+7BK+qbArsC5RO/NXYKJ1pj2vzNSZCyUFhPdG1g=="
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||
@@ -18149,9 +18164,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"ajv": {
|
||||
"version": "6.12.4",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz",
|
||||
"integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==",
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@zip.js/zip.js": "^2.3.6",
|
||||
"core-js": "^3.6.5",
|
||||
"echarts": "^5.1.2",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
<router-link :to="{ name: 'JobList' }">
|
||||
Jobs Status
|
||||
</router-link> |
|
||||
<router-link :to="{ name: 'Analysis' }">
|
||||
Analysis
|
||||
</router-link> |
|
||||
<router-link :to="{ name: 'About' }">
|
||||
About
|
||||
</router-link>
|
||||
|
||||
@@ -24,9 +24,6 @@
|
||||
import FormDate from '../services/FormDate.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormDate,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
|
||||
66
tools/otagui/src/components/OperationDetail.vue
Normal file
66
tools/otagui/src/components/OperationDetail.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
{{ mapType.get(operation.type) }}
|
||||
<p v-if="operation.dataOffset !== null">
|
||||
Data offset: {{ operation.dataOffset }}
|
||||
</p>
|
||||
<p v-if="operation.dataLength !== null">
|
||||
Data length: {{ operation.dataLength }}
|
||||
</p>
|
||||
<p v-if="operation.srcExtents !== null">
|
||||
Source: {{ operation.srcExtents.length }} extents ({{ srcTotalBlocks }}
|
||||
blocks)
|
||||
<br>
|
||||
{{ srcBlocks }}
|
||||
</p>
|
||||
<p v-if="operation.dstExtents !== null">
|
||||
Destination: {{ operation.dstExtents.length }} extents ({{ dstTotalBlocks }}
|
||||
blocks)
|
||||
<br>
|
||||
{{ dstBlocks }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
operation: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
mapType: {
|
||||
type: Map,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
srcTotalBlocks: null,
|
||||
srcBlocks: null,
|
||||
dstTotalBlocks: null,
|
||||
dstBlocks: null,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.operation.srcExtents) {
|
||||
this.srcTotalBlocks = numBlocks(this.operation.srcExtents)
|
||||
this.srcBlocks = displayBlocks(this.operation.srcExtents)
|
||||
}
|
||||
if (this.operation.dstExtents) {
|
||||
this.dstTotalBlocks = numBlocks(this.operation.dstExtents)
|
||||
this.dstBlocks = displayBlocks(this.operation.dstExtents)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function numBlocks(exts) {
|
||||
const accumulator = (total, ext) => total + ext.numBlocks
|
||||
return exts.reduce(accumulator, 0)
|
||||
}
|
||||
|
||||
function displayBlocks(exts) {
|
||||
const accumulator = (total, ext) =>
|
||||
total + '(' + ext.startBlock + ',' + ext.numBlocks + ')'
|
||||
return exts.reduce(accumulator, '')
|
||||
}
|
||||
|
||||
</script>
|
||||
60
tools/otagui/src/components/PartitionDetail.vue
Normal file
60
tools/otagui/src/components/PartitionDetail.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<p
|
||||
class="toggle"
|
||||
@click="toggle()"
|
||||
>
|
||||
Total Operations: {{ partition.operations.length }}
|
||||
<ul
|
||||
v-if="showOPs"
|
||||
>
|
||||
<li
|
||||
v-for="operation in partition.operations"
|
||||
:key="operation.dataSha256Hash"
|
||||
>
|
||||
<OperationDetail
|
||||
:operation="operation"
|
||||
:mapType="opType.mapType"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { OpType } from '@/services/payload.js'
|
||||
import OperationDetail from '@/components/OperationDetail.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
OperationDetail,
|
||||
},
|
||||
props: {
|
||||
partition: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showOPs: false,
|
||||
opType: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.opType = new OpType()
|
||||
},
|
||||
methods: {
|
||||
toggle() {
|
||||
this.showOPs = !this.showOPs
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.toggle {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
color: #00c255;
|
||||
}
|
||||
</style>
|
||||
81
tools/otagui/src/components/PayloadDetail.vue
Normal file
81
tools/otagui/src/components/PayloadDetail.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div v-if="zipFile">
|
||||
<h3>File infos</h3>
|
||||
<ul>
|
||||
<li>File name: {{ zipFile.name }}</li>
|
||||
<li>File size: {{ zipFile.size }} Bytes</li>
|
||||
<li>File last modified date: {{ zipFile.lastModifiedDate }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="payload">
|
||||
<h3>Partition List</h3>
|
||||
<ul v-if="payload.manifest">
|
||||
<li
|
||||
v-for="partition in payload.manifest.partitions"
|
||||
:key="partition.partitionName"
|
||||
>
|
||||
<h4>{{ partition.partitionName }}</h4>
|
||||
<p v-if="partition.estimateCowSize">
|
||||
Estimate COW Size: {{ partition.estimateCowSize }} Bytes
|
||||
</p>
|
||||
<p v-else>
|
||||
Estimate COW Size: 0 Bytes
|
||||
</p>
|
||||
<PartitionDetail :partition="partition" />
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Metadata Signature</h3>
|
||||
<div
|
||||
v-if="payload.metadata_signature"
|
||||
class="signature"
|
||||
>
|
||||
<span style="white-space: pre-wrap">
|
||||
{{ octToHex(payload.metadata_signature.signatures[0].data) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PartitionDetail from './PartitionDetail.vue'
|
||||
import { Payload } from '@/services/payload.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PartitionDetail,
|
||||
},
|
||||
props: {
|
||||
zipFile: {
|
||||
type: File,
|
||||
default: null,
|
||||
},
|
||||
payload: {
|
||||
type: Payload,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
octToHex: octToHex,
|
||||
},
|
||||
}
|
||||
|
||||
function octToHex(bufferArray) {
|
||||
let hex_table = ''
|
||||
for (let i = 0; i < bufferArray.length; i++) {
|
||||
hex_table += bufferArray[i].toString(16) + ' '
|
||||
if ((i + 1) % 16 == 0) {
|
||||
hex_table += '\n'
|
||||
}
|
||||
}
|
||||
return hex_table
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.signature {
|
||||
overflow: scroll;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import JobList from '@/views/JobList.vue'
|
||||
import JobDetails from '@/views/JobDetails.vue'
|
||||
import About from '@/views/About.vue'
|
||||
import SimpleForm from '@/views/SimpleForm.vue'
|
||||
import PackageAnalysis from '@/views/PackageAnalysis.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -25,6 +26,11 @@ const routes = [
|
||||
path: '/create',
|
||||
name: 'Create',
|
||||
component: SimpleForm
|
||||
},
|
||||
{
|
||||
path: '/analysis',
|
||||
name: 'Analysis',
|
||||
component: PackageAnalysis
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
131
tools/otagui/src/services/payload.js
Normal file
131
tools/otagui/src/services/payload.js
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @fileoverview Clss paypload is used to read in and
|
||||
* parse the payload.bin file from a OTA.zip file.
|
||||
* Class OpType creates a Map that can resolve the
|
||||
* operation type.
|
||||
* @package zip.js
|
||||
* @package protobufjs
|
||||
*/
|
||||
|
||||
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'
|
||||
|
||||
const _MAGIC = 'CrAU'
|
||||
const _VERSION_SIZE = 8
|
||||
const _MANIFEST_LEN_SIZE = 8
|
||||
const _METADATA_SIGNATURE_LEN_SIZE = 4
|
||||
const _BRILLO_MAJOR_PAYLOAD_VERSION = 2
|
||||
|
||||
export class Payload {
|
||||
/**
|
||||
* This class parses the metadata of a OTA package.
|
||||
* @param {File} file A OTA.zip file read from user's machine.
|
||||
*/
|
||||
constructor(file) {
|
||||
this.packedFile = new zip.ZipReader(new zip.BlobReader(file))
|
||||
this.cursor = 0
|
||||
}
|
||||
|
||||
async unzipPayload() {
|
||||
let entries = await this.packedFile.getEntries()
|
||||
this.payload = null
|
||||
for (let entry of entries) {
|
||||
if (entry.filename == 'payload.bin') {
|
||||
//TODO: only read in the manifest instead of the whole payload
|
||||
this.payload = await entry.getData(new zip.BlobWriter())
|
||||
}
|
||||
}
|
||||
if (!this.payload) {
|
||||
alert('Please select a legit OTA package')
|
||||
return
|
||||
}
|
||||
this.buffer = await this.payload.arrayBuffer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Read in an integer from binary bufferArray.
|
||||
* @param {Int} size the size of a integer being read in
|
||||
* @return {Int} an integer.
|
||||
*/
|
||||
readInt(size) {
|
||||
let view = new DataView(
|
||||
this.buffer.slice(this.cursor, this.cursor + size))
|
||||
this.cursor += size
|
||||
switch (size) {
|
||||
case 2:
|
||||
return view.getUInt16(0)
|
||||
case 4:
|
||||
return view.getUint32(0)
|
||||
case 8:
|
||||
return Number(view.getBigUint64(0))
|
||||
default:
|
||||
throw 'Cannot read this integer with size ' + size
|
||||
}
|
||||
}
|
||||
|
||||
readHeader() {
|
||||
let decoder = new TextDecoder()
|
||||
try {
|
||||
this.magic = decoder.decode(
|
||||
this.buffer.slice(this.cursor, _MAGIC.length))
|
||||
this.cursor += _MAGIC.length
|
||||
if (this.magic != _MAGIC) {
|
||||
alert('MAGIC is not correct, please double check.')
|
||||
}
|
||||
this.header_version = this.readInt(_VERSION_SIZE)
|
||||
this.manifest_len = this.readInt(_MANIFEST_LEN_SIZE)
|
||||
if (this.header_version == _BRILLO_MAJOR_PAYLOAD_VERSION) {
|
||||
this.metadata_signature_len = this.readInt(_METADATA_SIGNATURE_LEN_SIZE)
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read in the manifest in an OTA.zip file.
|
||||
* The structure of the manifest can be found in:
|
||||
* aosp/system/update_engine/update_metadata.proto
|
||||
*/
|
||||
readManifest() {
|
||||
let manifest_raw = new Uint8Array(this.buffer.slice(
|
||||
this.cursor, this.cursor + this.manifest_len
|
||||
))
|
||||
this.cursor += this.manifest_len
|
||||
this.manifest = update_metadata_pb.DeltaArchiveManifest
|
||||
.decode(manifest_raw)
|
||||
}
|
||||
|
||||
readSignature() {
|
||||
let signature_raw = new Uint8Array(this.buffer.slice(
|
||||
this.cursor, this.cursor + this.metadata_signature_len
|
||||
))
|
||||
this.cursor += this.metadata_signature_len
|
||||
this.metadata_signature = update_metadata_pb.Signatures
|
||||
.decode(signature_raw)
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.unzipPayload()
|
||||
this.readHeader()
|
||||
this.readManifest()
|
||||
this.readSignature()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class OpType {
|
||||
/**
|
||||
* OpType.mapType create a map that could resolve the operation
|
||||
* types. The operation types are encoded as numbers in
|
||||
* update_metadata.proto and must be decoded before any usage.
|
||||
*/
|
||||
constructor() {
|
||||
let types = update_metadata_pb.InstallOperation.Type
|
||||
this.mapType = new Map()
|
||||
for (let key in types) {
|
||||
this.mapType.set(types[key], key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,14 @@ import JobConfiguration from '../components/JobConfiguration.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ApiService,
|
||||
JobConfiguration,
|
||||
},
|
||||
props: ['id'],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const stderr = ref()
|
||||
const stdout = ref()
|
||||
|
||||
44
tools/otagui/src/views/PackageAnalysis.vue
Normal file
44
tools/otagui/src/views/PackageAnalysis.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<BaseFile
|
||||
label="Select an OTA package"
|
||||
@file-select="unpackOTA"
|
||||
/>
|
||||
<PayloadDetail
|
||||
v-if="zipFile && payload"
|
||||
:zipFile="zipFile"
|
||||
:payload="payload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseFile from '@/components/BaseFile.vue'
|
||||
import PayloadDetail from '@/components/PayloadDetail.vue'
|
||||
import { Payload } from '@/services/payload.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseFile,
|
||||
PayloadDetail,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
zipFile: null,
|
||||
payload: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async unpackOTA(files) {
|
||||
this.zipFile = files[0]
|
||||
try {
|
||||
this.payload = new Payload(this.zipFile)
|
||||
await this.payload.init()
|
||||
} catch (err) {
|
||||
alert('Please check if this is a correct OTA package (.zip).')
|
||||
console.log(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -112,7 +112,6 @@ export default {
|
||||
UploadFile,
|
||||
FileSelect,
|
||||
PartialCheckbox,
|
||||
FormDate,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user