1/**
2 * @fileoverview Class paypload is used to read in and
3 * parse the payload.bin file from a OTA.zip file.
4 * Class OpType creates a Map that can resolve the
5 * operation type.
6 * @package zip.js
7 * @package protobufjs
8 */
9
10import * as zip from '@zip.js/zip.js/dist/zip-full.min.js'
11import { chromeos_update_engine as update_metadata_pb } from './update_metadata_pb.js'
12import { PayloadNonAB } from './payload_nonab.js'
13
14const /** String */ _MAGIC = 'CrAU'
15const /** Number */ _VERSION_SIZE = 8
16const /** Number */ _MANIFEST_LEN_SIZE = 8
17const /** Number */ _METADATA_SIGNATURE_LEN_SIZE = 4
18
19const /** Number */ _PAYLOAD_HEADER_SIZE =
20  _MAGIC.length + _VERSION_SIZE + _MANIFEST_LEN_SIZE + _METADATA_SIGNATURE_LEN_SIZE
21
22const /** Number */ _BRILLO_MAJOR_PAYLOAD_VERSION = 2
23export const /** Array<Object> */ MetadataFormat = [
24  {
25    prefix: 'pre-build',
26    key: 'preBuild',
27    name: 'Pre-build'
28  },
29  {
30    prefix: 'pre-build-incremental',
31    key: 'preBuildVersion',
32    name: 'Pre-build version'
33  },
34  {
35    prefix: 'post-build',
36    key: 'postBuild',
37    name: 'Post-build'
38  },
39  {
40    prefix: 'post-build-incremental',
41    key: 'postBuildVersion',
42    name: 'Post-build version'
43  }
44]
45
46class StopIteration extends Error {
47
48}
49
50class OTAPayloadBlobWriter extends zip.Writer {
51  /**
52   * A zip.Writer that is tailored for OTA payload.bin read-in.
53   * Instead of reading in all the contents in payload.bin, this writer will
54   * throw an 'StopIteration' error when the header is read in.
55   * The header will be stored into the <payload>.
56   * @param {Payload} payload
57   * @param {String} contentType
58   */
59  constructor(payload, contentType = "") {
60    super()
61    this.offset = 0
62    this.contentType = contentType
63    this.blob = new Blob([], { type: contentType })
64    this.prefixLength = 0
65    this.payload = payload
66  }
67
68  async writeUint8Array(/**  @type {Uint8Array} */ array) {
69    super.writeUint8Array(array)
70    this.blob = new Blob([this.blob, array.buffer], { type: this.contentType })
71    this.offset = this.blob.size
72    // Once the prefixLength is non-zero, the address of manifest and signature
73    // become known and can be read in. Otherwise the header needs to be read
74    // in first to determine the prefixLength.
75    if (this.offset >= _PAYLOAD_HEADER_SIZE) {
76      await this.payload.readHeader(this.blob)
77      this.prefixLength =
78        _PAYLOAD_HEADER_SIZE
79        + this.payload.manifest_len
80        + this.payload.metadata_signature_len
81      console.log(`Computed metadata length: ${this.prefixLength}`);
82    }
83    if (this.prefixLength > 0) {
84      console.log(`${this.offset}/${this.prefixLength}`);
85      if (this.offset >= this.prefixLength) {
86        await this.payload.readManifest(this.blob)
87        await this.payload.readSignature(this.blob)
88      }
89    }
90    // The prefix has everything we need (header, manifest, signature). Once
91    // the offset is beyond the prefix, no need to move on.
92    if (this.offset >= this.prefixLength) {
93      throw new StopIteration()
94    }
95  }
96
97  getData() {
98    return this.blob
99  }
100}
101
102export class Payload {
103  /**
104   * This class parses the metadata of a OTA package.
105   * @param {File} file A OTA.zip file read from user's machine.
106   */
107  constructor(file) {
108    this.packedFile = new zip.ZipReader(new zip.BlobReader(file))
109    this.cursor = 0
110  }
111
112  /**
113   * Unzip the OTA package, get payload.bin and metadata
114   */
115  async unzip() {
116    let /** Array<Entry> */ entries = await this.packedFile.getEntries()
117    this.payload = null
118    for (let entry of entries) {
119      if (entry.filename == 'payload.bin') {
120        let writer = new OTAPayloadBlobWriter(this, "")
121        try {
122          await entry.getData(writer)
123        } catch (e) {
124          if (e instanceof StopIteration) {
125            // Exception used as a hack to stop reading from zip. NO need to do anything
126            // Ideally zip.js would provide an API to partialll read a zip
127            // entry, but they don't. So this is what we get
128          } else {
129            throw e
130          }
131        }
132        this.payload = writer.getData()
133        break
134      }
135      if (entry.filename == 'META-INF/com/android/metadata') {
136        this.metadata = await entry.getData(new zip.TextWriter())
137      }
138    }
139    if (!this.payload) {
140      try {
141        // The temporary variable manifest has to be used here, to prevent the html page
142        // being rendered before everything is read in properly
143        let manifest = new PayloadNonAB(this.packedFile)
144        await manifest.init()
145        manifest.nonAB = true
146        this.manifest = manifest
147      } catch (error) {
148        alert('Please select a legit OTA package')
149        return
150      }
151    }
152  }
153
154  /**
155   * Read in an integer from binary bufferArray.
156   * @param {Int} size the size of a integer being read in
157   * @return {Int} an integer.
158   */
159  readInt(size) {
160    let /** DataView */ view = new DataView(
161      this.buffer.slice(this.cursor, this.cursor + size))
162    if (typeof view.getBigUint64 !== "function") {
163      view.getBigUint64 =
164        function (offset) {
165          const a = BigInt(view.getUint32(offset))
166          const b = BigInt(view.getUint32(offset + 4))
167          const bigNumber = a * 4294967296n + b
168          return bigNumber
169        }
170    }
171    this.cursor += size
172    switch (size) {
173    case 2:
174      return view.getUInt16(0)
175    case 4:
176      return view.getUint32(0)
177    case 8:
178      return Number(view.getBigUint64(0))
179    default:
180      throw 'Cannot read this integer with size ' + size
181    }
182  }
183
184  /**
185   * Read the header of payload.bin, including the magic, header_version,
186   * manifest_len, metadata_signature_len.
187   */
188  async readHeader(/** @type {Blob} */buffer) {
189    this.buffer = await buffer.slice(0, _PAYLOAD_HEADER_SIZE).arrayBuffer()
190    let /** TextDecoder */ decoder = new TextDecoder()
191    try {
192      this.magic = decoder.decode(
193        this.buffer.slice(this.cursor, _MAGIC.length))
194      if (this.magic != _MAGIC) {
195        throw new Error('MAGIC is not correct, please double check.')
196      }
197      this.cursor += _MAGIC.length
198      this.header_version = this.readInt(_VERSION_SIZE)
199      this.manifest_len = this.readInt(_MANIFEST_LEN_SIZE)
200      if (this.header_version == _BRILLO_MAJOR_PAYLOAD_VERSION) {
201        this.metadata_signature_len = this.readInt(_METADATA_SIGNATURE_LEN_SIZE)
202      }
203      else {
204        throw new Error(`Unexpected major version number: ${this.header_version}`)
205      }
206    } catch (err) {
207      console.log(err)
208      return
209    }
210  }
211
212  /**
213   * Read in the manifest in an OTA.zip file.
214   * The structure of the manifest can be found in:
215   * aosp/system/update_engine/update_metadata.proto
216   */
217  async readManifest(/** @type {Blob} */buffer) {
218    buffer = await buffer.slice(
219      this.cursor, this.cursor + this.manifest_len).arrayBuffer()
220    this.cursor += this.manifest_len
221    this.manifest = update_metadata_pb.DeltaArchiveManifest
222      .decode(new Uint8Array(buffer))
223    this.manifest.nonAB = false
224  }
225
226  async readSignature(/** @type {Blob} */buffer) {
227    buffer = await buffer.slice(
228      this.cursor, this.cursor + this.metadata_signature_len).arrayBuffer()
229    this.cursor += this.metadata_signature_len
230    this.metadata_signature = update_metadata_pb.Signatures
231      .decode(new Uint8Array(buffer))
232  }
233
234  parseMetadata() {
235    for (let formatter of MetadataFormat) {
236      let regex = new RegExp(formatter.prefix + '.+')
237      if (this.metadata.match(regex)) {
238        this[formatter.key] =
239          trimEntry(this.metadata.match(regex)[0], formatter.prefix)
240      } else this[formatter.key] = ''
241    }
242  }
243
244  async init() {
245    await this.unzip()
246    this.parseMetadata()
247  }
248
249}
250
251export class DefaultMap extends Map {
252  /** Reload the original get method. Return the original key value if
253   * the key does not exist.
254   * @param {Any} key
255   */
256  getWithDefault(key) {
257    if (!this.has(key)) return key
258    return this.get(key)
259  }
260}
261
262export class OpType {
263  /**
264   * OpType.mapType create a map that could resolve the operation
265   * types. The operation types are encoded as numbers in
266   * update_metadata.proto and must be decoded before any usage.
267   */
268  constructor() {
269    let /** Array<{String: Number}>*/ types = update_metadata_pb.InstallOperation.Type
270    this.mapType = new DefaultMap()
271    for (let key of Object.keys(types)) {
272      this.mapType.set(types[key], key)
273    }
274  }
275}
276
277export class MergeOpType {
278  /**
279   * MergeOpType create a map that could resolve the COW merge operation
280   * types. This is very similar to OpType class except that one is for
281   * installation operations.
282   */
283  constructor() {
284    let /** Array<{String: Number}>*/ types =
285      update_metadata_pb.CowMergeOperation.Type
286    this.mapType = new DefaultMap()
287    for (let key of Object.keys(types)) {
288      this.mapType.set(types[key], key)
289    }
290  }
291}
292
293export function octToHex(bufferArray, space = true, maxLine = 16) {
294  let hex_table = ''
295  for (let i = 0; i < bufferArray.length; i++) {
296    if (bufferArray[i].toString(16).length === 2) {
297      hex_table += bufferArray[i].toString(16) + (space ? ' ' : '')
298    } else {
299      hex_table += '0' + bufferArray[i].toString(16) + (space ? ' ' : '')
300    }
301    if ((i + 1) % maxLine == 0) {
302      hex_table += '\n'
303    }
304  }
305  return hex_table
306}
307
308/**
309 * Trim the prefix in an entry. This is required because the lookbehind
310 * regular expression is not supported in safari yet.
311 * @param {String} entry
312 * @param {String} prefix
313 * @return String
314 */
315function trimEntry(entry, prefix) {
316  return entry.slice(prefix.length + 1, entry.length)
317}