Merge "Add support for analysis of OTA package."

This commit is contained in:
Treehugger Robot
2021-06-28 20:39:27 +00:00
committed by Gerrit Code Review
13 changed files with 438 additions and 27 deletions

View File

@@ -11,7 +11,11 @@
"plugin:vue/recommended" "plugin:vue/recommended"
], ],
"rules": { "rules": {
"indent": ["error", 2], "indent": [
"vue/no-multiple-template-root": 0 "error",
2
],
"vue/no-multiple-template-root": 0,
"vue/attribute-hyphenation": 0
} }
} }

View File

@@ -8,6 +8,7 @@
"name": "OTA_GUI", "name": "OTA_GUI",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@zip.js/zip.js": "^2.3.6",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"echarts": "^5.1.2", "echarts": "^5.1.2",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",
@@ -2944,6 +2945,11 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true "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": { "node_modules/accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -2995,14 +3001,18 @@
} }
}, },
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.4", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1", "json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2" "uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/ajv-errors": { "node_modules/ajv-errors": {
@@ -18115,6 +18125,11 @@
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
"dev": true "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": { "accepts": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -18149,9 +18164,9 @@
"dev": true "dev": true
}, },
"ajv": { "ajv": {
"version": "6.12.4", "version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": { "requires": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",

View File

@@ -8,6 +8,7 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@zip.js/zip.js": "^2.3.6",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"echarts": "^5.1.2", "echarts": "^5.1.2",
"eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-base": "^14.2.1",

View File

@@ -7,6 +7,9 @@
<router-link :to="{ name: 'JobList' }"> <router-link :to="{ name: 'JobList' }">
Jobs Status Jobs Status
</router-link> | </router-link> |
<router-link :to="{ name: 'Analysis' }">
Analysis
</router-link> |
<router-link :to="{ name: 'About' }"> <router-link :to="{ name: 'About' }">
About About
</router-link> </router-link>

View File

@@ -24,9 +24,6 @@
import FormDate from '../services/FormDate.js' import FormDate from '../services/FormDate.js'
export default { export default {
components: {
FormDate,
},
props: { props: {
job: { job: {
type: Object, type: Object,

View 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>

View 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>

View 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>

View File

@@ -3,6 +3,7 @@ import JobList from '@/views/JobList.vue'
import JobDetails from '@/views/JobDetails.vue' import JobDetails from '@/views/JobDetails.vue'
import About from '@/views/About.vue' import About from '@/views/About.vue'
import SimpleForm from '@/views/SimpleForm.vue' import SimpleForm from '@/views/SimpleForm.vue'
import PackageAnalysis from '@/views/PackageAnalysis.vue'
const routes = [ const routes = [
{ {
@@ -25,6 +26,11 @@ const routes = [
path: '/create', path: '/create',
name: 'Create', name: 'Create',
component: SimpleForm component: SimpleForm
},
{
path: '/analysis',
name: 'Analysis',
component: PackageAnalysis
} }
] ]

View 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)
}
}
}

View File

@@ -38,10 +38,14 @@ import JobConfiguration from '../components/JobConfiguration.vue'
export default { export default {
components: { components: {
ApiService,
JobConfiguration, JobConfiguration,
}, },
props: ['id'], props: {
id: {
type: String,
required: true
}
},
setup() { setup() {
const stderr = ref() const stderr = ref()
const stdout = ref() const stdout = ref()

View 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>

View File

@@ -112,7 +112,6 @@ export default {
UploadFile, UploadFile,
FileSelect, FileSelect,
PartialCheckbox, PartialCheckbox,
FormDate,
}, },
data() { data() {
return { return {