diff --git a/tools/otagui/.eslintrc.json b/tools/otagui/.eslintrc.json
index 416c168f8..82ff479a3 100644
--- a/tools/otagui/.eslintrc.json
+++ b/tools/otagui/.eslintrc.json
@@ -1,17 +1,21 @@
{
- "env": {
- "browser": true,
- "es6": true,
- "node": true
- },
- "parserOptions": {
- "parser": "babel-eslint"
- },
- "extends": [
- "plugin:vue/recommended"
+ "env": {
+ "browser": true,
+ "es6": true,
+ "node": true
+ },
+ "parserOptions": {
+ "parser": "babel-eslint"
+ },
+ "extends": [
+ "plugin:vue/recommended"
+ ],
+ "rules": {
+ "indent": [
+ "error",
+ 2
],
- "rules": {
- "indent": ["error", 2],
- "vue/no-multiple-template-root": 0
- }
+ "vue/no-multiple-template-root": 0,
+ "vue/attribute-hyphenation": 0
+ }
}
\ No newline at end of file
diff --git a/tools/otagui/package-lock.json b/tools/otagui/package-lock.json
index 84c6b6045..754d3fc58 100644
--- a/tools/otagui/package-lock.json
+++ b/tools/otagui/package-lock.json
@@ -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",
diff --git a/tools/otagui/package.json b/tools/otagui/package.json
index 41ba6b131..158081765 100644
--- a/tools/otagui/package.json
+++ b/tools/otagui/package.json
@@ -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",
diff --git a/tools/otagui/src/App.vue b/tools/otagui/src/App.vue
index 7b8362856..60d35ade3 100644
--- a/tools/otagui/src/App.vue
+++ b/tools/otagui/src/App.vue
@@ -6,7 +6,10 @@
|
Jobs Status
- |
+ |
+
+ Analysis
+ |
About
diff --git a/tools/otagui/src/components/JobConfiguration.vue b/tools/otagui/src/components/JobConfiguration.vue
index 17e660637..e1fc1539a 100644
--- a/tools/otagui/src/components/JobConfiguration.vue
+++ b/tools/otagui/src/components/JobConfiguration.vue
@@ -24,9 +24,6 @@
import FormDate from '../services/FormDate.js'
export default {
- components: {
- FormDate,
- },
props: {
job: {
type: Object,
diff --git a/tools/otagui/src/components/OperationDetail.vue b/tools/otagui/src/components/OperationDetail.vue
new file mode 100644
index 000000000..e2626e535
--- /dev/null
+++ b/tools/otagui/src/components/OperationDetail.vue
@@ -0,0 +1,66 @@
+
+ {{ mapType.get(operation.type) }}
+
+ Data offset: {{ operation.dataOffset }}
+
+
+ Data length: {{ operation.dataLength }}
+
+
+ Source: {{ operation.srcExtents.length }} extents ({{ srcTotalBlocks }}
+ blocks)
+
+ {{ srcBlocks }}
+
+
+ Destination: {{ operation.dstExtents.length }} extents ({{ dstTotalBlocks }}
+ blocks)
+
+ {{ dstBlocks }}
+
+
+
+
\ No newline at end of file
diff --git a/tools/otagui/src/components/PartitionDetail.vue b/tools/otagui/src/components/PartitionDetail.vue
new file mode 100644
index 000000000..de27a5dc6
--- /dev/null
+++ b/tools/otagui/src/components/PartitionDetail.vue
@@ -0,0 +1,60 @@
+
+
+ Total Operations: {{ partition.operations.length }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/otagui/src/components/PayloadDetail.vue b/tools/otagui/src/components/PayloadDetail.vue
new file mode 100644
index 000000000..245be24a1
--- /dev/null
+++ b/tools/otagui/src/components/PayloadDetail.vue
@@ -0,0 +1,81 @@
+
+
+
File infos
+
+ - File name: {{ zipFile.name }}
+ - File size: {{ zipFile.size }} Bytes
+ - File last modified date: {{ zipFile.lastModifiedDate }}
+
+
+
+
Partition List
+
+
Metadata Signature
+
+
+ {{ octToHex(payload.metadata_signature.signatures[0].data) }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/otagui/src/router/index.js b/tools/otagui/src/router/index.js
index 6f3c3a2fd..4673c2f70 100644
--- a/tools/otagui/src/router/index.js
+++ b/tools/otagui/src/router/index.js
@@ -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
}
]
diff --git a/tools/otagui/src/services/payload.js b/tools/otagui/src/services/payload.js
new file mode 100644
index 000000000..e51d656e0
--- /dev/null
+++ b/tools/otagui/src/services/payload.js
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/otagui/src/views/JobDetails.vue b/tools/otagui/src/views/JobDetails.vue
index 89abc0901..8baf9b561 100644
--- a/tools/otagui/src/views/JobDetails.vue
+++ b/tools/otagui/src/views/JobDetails.vue
@@ -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()
diff --git a/tools/otagui/src/views/PackageAnalysis.vue b/tools/otagui/src/views/PackageAnalysis.vue
new file mode 100644
index 000000000..538b6cf2f
--- /dev/null
+++ b/tools/otagui/src/views/PackageAnalysis.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
diff --git a/tools/otagui/src/views/SimpleForm.vue b/tools/otagui/src/views/SimpleForm.vue
index b6f6b045e..809f80f63 100644
--- a/tools/otagui/src/views/SimpleForm.vue
+++ b/tools/otagui/src/views/SimpleForm.vue
@@ -112,7 +112,6 @@ export default {
UploadFile,
FileSelect,
PartialCheckbox,
- FormDate,
},
data() {
return {