1import * as zip from '@zip.js/zip.js/dist/zip-full.min.js'
2import { chromeos_update_engine } from './update_metadata_pb.js'
3
4/**
5 * Parse the non-A/B OTA package and return it as a DeltaArchiveManifest
6 * @param {ZipReader} packedFile
7 */
8export class PayloadNonAB extends chromeos_update_engine.DeltaArchiveManifest{
9  constructor(packedFile) {
10    super()
11    this.packedFile = packedFile
12  }
13
14  async init() {
15    this.Blocksize = 4096
16    this.partialUpdate = false
17    this.dynamicPartitionMetadata =
18      new chromeos_update_engine.DynamicPartitionMetadata(
19        { snapshotEnabled: false, vabcEnabled: false }
20      )
21    this.partitions = []
22
23    const /** RegExp*/ regexName = /[\w_]+(?=\.transfer.list)/g
24    const /** Array<Entry> */ entries = await this.packedFile.getEntries()
25    for (const entry of entries) {
26      if (entry.filename.match(regexName)) {
27        let newPartition = new chromeos_update_engine.PartitionUpdate(
28          {partitionName: entry.filename.match(regexName)[0]}
29        )
30        newPartition.rawText = await entry.getData(new zip.TextWriter())
31        await this.parseTransferList(newPartition)
32        this.partitions.push(newPartition)
33      }
34    }
35  }
36
37  async parseTransferList(partition) {
38    let /** Array<String> */ lines = partition.rawText.split('\n')
39    // First four line in header: version, total blocks,
40    // number of stashed entries, maximum used memory for stash
41    if (lines.length < 4) {
42      throw "At least 4 lines in header should be provided."
43    }
44    partition.version = parseInt(lines[0])
45    partition.totalBlocks = parseInt(lines[1])
46    partition.entryStashed = parseInt(lines[2])
47    partition.maxStashed = parseInt(lines[3])
48    partition.newPartitionInfo = new chromeos_update_engine.PartitionInfo()
49    partition.newPartitionInfo.hash = ''
50    partition.newPartitionInfo.size = 'unknown'
51    /**
52    * The main body have 8 different ops:
53    * zero [rangeset] : fill zeros
54    * new [rangeset] : fill with new data from <partitionName.new.data>
55    * erase [rangeset] : mark given blocks as empty
56    * move <src_hash> <...>
57    * bsdiff <patchstart> <patchlen> <src_hash> <tgt_hash> <...>
58    * imgdiff <patchstart> <patchlen> <src_hash> <tgt_hash> <...> :
59    * Read the source blocks and apply (not for move op) to the target blocks
60    * stash <stash_id> <src_range> : load the given source range to memory
61    * free <stash_id> : free the given <stash_id>
62    * format:
63    * [rangeset]: <# of pairs>, <pair A start>, <pair A end>, ...
64    * <stash_id>: a hex number with length of 40
65    * <...>: We expect to parse the remainder of the parameter tokens as one of:
66    *   <tgt_range> <src_block_count> <src_range> (loads data from source image only)
67    *   <tgt_range> <src_block_count> - <[stash_id:stash_range] ...> (loads data from stashes only)
68    *   <tgt_range> <src_block_count> <src_range> <src_loc> <[stash_id:stash_range] ...>
69    *   (loads data from both source image and stashes)
70    */
71    partition.operations = new Array()
72    let newDataSize = await this.sizeNewData(partition.partitionName)
73    for (const line of lines.slice(4)) {
74      let op = new chromeos_update_engine.InstallOperation()
75      let elements = line.split(' ')
76      op.type = elements[0]
77      switch (op.type) {
78      case 'zero':
79        op.dstExtents = elements.slice(1).reduce(parseRange, [])
80        break
81      case 'new':
82        // unlike an A/B OTA, the payload only exists in the payload.bin,
83        // in an non-A/B OTA, the payload exists in both .new.data and .patch.data.
84        // The new ops do not have any information about data length.
85        // what we do here is: read in the size of .new.data, assume the first new
86        // op have the data length of the size of .new.data.
87        op.dataLength = newDataSize
88        newDataSize = 0
89        op.dstExtents = elements.slice(1).reduce(parseRange, [])
90        break
91      case 'erase':
92        op.dstExtents = elements.slice(1).reduce(parseRange, [])
93        break
94      case 'move':
95        op.dstExtents = parseRange([], elements[2])
96        break
97      case 'bsdiff':
98        op.dataOffset = parseInt(elements[1])
99        op.dataLength = parseInt(elements[2])
100        op.dstExtents = parseRange([], elements[5])
101        break
102      case 'imgdiff':
103        op.dataOffset = parseInt(elements[1])
104        op.dataLength = parseInt(elements[2])
105        op.dstExtents = parseRange([], elements[5])
106        break
107      case 'stash':
108        break
109      case 'free':
110        break
111      }
112      partition.operations.push(op)
113    }
114  }
115
116  /**
117   * Return the size of <partitionName>.new.data.*
118   * @param {String} partitionName
119   * @return {Number}
120   */
121  async sizeNewData(partitionName) {
122    const /** Array<Entry> */ entries = await this.packedFile.getEntries()
123    const /** RegExp */ regexName = new RegExp(partitionName + '.new.dat.*')
124    for (const entry of entries) {
125      if (entry.filename.match(regexName)) {
126        return entry.uncompressedSize
127      }
128    }
129  }
130}
131
132/**
133 * Parse the range string and return it as an array of extents
134 * @param {extents} Array<extents>
135 * @param {String} rangeset
136 * @return Array<extents>
137 */
138function parseRange(extents, rangeset) {
139  const regexRange = new RegExp('[\d,]+')
140  if (rangeset.match(regexRange)) {
141    let elements = rangeset.split(',')
142    for (let i=1; i<elements.length; i=i+2) {
143      let extent = new Object(
144        {
145          startBlock: parseInt(elements[i]),
146          numBlocks: parseInt(elements[i+1]) - parseInt(elements[i])
147        }
148      )
149      extents.push(extent)
150    }
151  }
152  return extents
153}