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}