1# Copyright (C) 2009 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import re
16
17import common
18
19class EdifyGenerator(object):
20  """Class to generate scripts in the 'edify' recovery script language
21  used from donut onwards."""
22
23  def __init__(self, version, info, fstab=None):
24    self.script = []
25    self.mounts = set()
26    self._required_cache = 0
27    self.version = version
28    self.info = info
29    if fstab is None:
30      self.fstab = self.info.get("fstab", None)
31    else:
32      self.fstab = fstab
33
34  @property
35  def required_cache(self):
36    """Return the minimum cache size to apply the update."""
37    return self._required_cache
38
39  @staticmethod
40  def WordWrap(cmd, linelen=80):
41    """'cmd' should be a function call with null characters after each
42    parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
43    to a given line length, replacing nulls with spaces and/or newlines
44    to format it nicely."""
45    indent = cmd.index("(")+1
46    out = []
47    first = True
48    x = re.compile("^(.{,%d})\0" % (linelen-indent,))
49    while True:
50      if not first:
51        out.append(" " * indent)
52      first = False
53      m = x.search(cmd)
54      if not m:
55        parts = cmd.split("\0", 1)
56        out.append(parts[0]+"\n")
57        if len(parts) == 1:
58          break
59        else:
60          cmd = parts[1]
61          continue
62      out.append(m.group(1)+"\n")
63      cmd = cmd[m.end():]
64
65    return "".join(out).replace("\0", " ").rstrip("\n")
66
67  def AppendScript(self, other):
68    """Append the contents of another script (which should be created
69    with temporary=True) to this one."""
70    self.script.extend(other.script)
71
72  def AssertOemProperty(self, name, values, oem_no_mount):
73    """Assert that a property on the OEM paritition matches allowed values."""
74    if not name:
75      raise ValueError("must specify an OEM property")
76    if not values:
77      raise ValueError("must specify the OEM value")
78
79    if oem_no_mount:
80      get_prop_command = 'getprop("%s")' % name
81    else:
82      get_prop_command = 'file_getprop("/oem/oem.prop", "%s")' % name
83
84    cmd = ''
85    for value in values:
86      cmd += '%s == "%s" || ' % (get_prop_command, value)
87    cmd += (
88        'abort("E{code}: This package expects the value \\"{values}\\" for '
89        '\\"{name}\\"; this has value \\"" + '
90        '{get_prop_command} + "\\".");').format(
91            code=common.ErrorCode.OEM_PROP_MISMATCH,
92            get_prop_command=get_prop_command, name=name,
93            values='\\" or \\"'.join(values))
94    self.script.append(cmd)
95
96  def AssertSomeFingerprint(self, *fp):
97    """Assert that the current recovery build fingerprint is one of *fp."""
98    if not fp:
99      raise ValueError("must specify some fingerprints")
100    cmd = (' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"') % i
101                             for i in fp]) +
102           ' ||\n    abort("E%d: Package expects build fingerprint of %s; '
103           'this device has " + getprop("ro.build.fingerprint") + ".");') % (
104               common.ErrorCode.FINGERPRINT_MISMATCH, " or ".join(fp))
105    self.script.append(cmd)
106
107  def AssertSomeThumbprint(self, *fp):
108    """Assert that the current recovery build thumbprint is one of *fp."""
109    if not fp:
110      raise ValueError("must specify some thumbprints")
111    cmd = (' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"') % i
112                             for i in fp]) +
113           ' ||\n    abort("E%d: Package expects build thumbprint of %s; this '
114           'device has " + getprop("ro.build.thumbprint") + ".");') % (
115               common.ErrorCode.THUMBPRINT_MISMATCH, " or ".join(fp))
116    self.script.append(cmd)
117
118  def AssertFingerprintOrThumbprint(self, fp, tp):
119    """Assert that the current recovery build fingerprint is fp, or thumbprint
120       is tp."""
121    cmd = ('getprop("ro.build.fingerprint") == "{fp}" ||\n'
122           '    getprop("ro.build.thumbprint") == "{tp}" ||\n'
123           '    abort("Package expects build fingerprint of {fp} or '
124           'thumbprint of {tp}; this device has a fingerprint of " '
125           '+ getprop("ro.build.fingerprint") + " and a thumbprint of " '
126           '+ getprop("ro.build.thumbprint") + ".");').format(fp=fp, tp=tp)
127    self.script.append(cmd)
128
129  def AssertOlderBuild(self, timestamp, timestamp_text):
130    """Assert that the build on the device is older (or the same as)
131    the given timestamp."""
132    self.script.append(
133        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
134         'abort("E%d: Can\'t install this package (%s) over newer '
135         'build (" + getprop("ro.build.date") + ").");') % (
136             timestamp, common.ErrorCode.OLDER_BUILD, timestamp_text))
137
138  def AssertDevice(self, device):
139    """Assert that the device identifier is the given string."""
140    cmd = ('getprop("ro.product.device") == "%s" || '
141           'abort("E%d: This package is for \\"%s\\" devices; '
142           'this is a \\"" + getprop("ro.product.device") + "\\".");') % (
143               device, common.ErrorCode.DEVICE_MISMATCH, device)
144    self.script.append(cmd)
145
146  def AssertSomeBootloader(self, *bootloaders):
147    """Asert that the bootloader version is one of *bootloaders."""
148    cmd = ("assert(" +
149           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
150                         for b in bootloaders]) +
151           ");")
152    self.script.append(self.WordWrap(cmd))
153
154  def ShowProgress(self, frac, dur):
155    """Update the progress bar, advancing it over 'frac' over the next
156    'dur' seconds.  'dur' may be zero to advance it via SetProgress
157    commands instead of by time."""
158    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
159
160  def SetProgress(self, frac):
161    """Set the position of the progress bar within the chunk defined
162    by the most recent ShowProgress call.  'frac' should be in
163    [0,1]."""
164    self.script.append("set_progress(%f);" % (frac,))
165
166  def PatchCheck(self, filename, *sha1):  # pylint: disable=unused-argument
167    """Checks that the given partition has the desired checksum.
168
169    The call to this function is being deprecated in favor of
170    PatchPartitionCheck(). It will try to parse and handle the old format,
171    unless the format is unknown.
172    """
173    tokens = filename.split(':')
174    assert len(tokens) == 6 and tokens[0] == 'EMMC', \
175        "Failed to handle unknown format. Use PatchPartitionCheck() instead."
176    source = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[2], tokens[3])
177    target = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[4], tokens[5])
178    self.PatchPartitionCheck(target, source)
179
180  def PatchPartitionCheck(self, target, source):
181    """Checks whether updater can patch the given partitions.
182
183    It checks the checksums of the given partitions. If none of them matches the
184    expected checksum, updater will additionally look for a backup on /cache.
185    """
186    self._CheckSecondTokenNotSlotSuffixed(target, "PatchPartitionExprCheck")
187    self._CheckSecondTokenNotSlotSuffixed(source, "PatchPartitionExprCheck")
188    self.PatchPartitionExprCheck('"%s"' % target, '"%s"' % source)
189
190  def PatchPartitionExprCheck(self, target_expr, source_expr):
191    """Checks whether updater can patch the given partitions.
192
193    It checks the checksums of the given partitions. If none of them matches the
194    expected checksum, updater will additionally look for a backup on /cache.
195
196    Args:
197      target_expr: an Edify expression that serves as the target arg to
198        patch_partition. Must be evaluated to a string in the form of
199        foo:bar:baz:quux
200      source_expr: an Edify expression that serves as the source arg to
201        patch_partition. Must be evaluated to a string in the form of
202        foo:bar:baz:quux
203    """
204    self.script.append(self.WordWrap((
205        'patch_partition_check({target},\0{source}) ||\n    abort('
206        'concat("E{code}: \\"",{target},"\\" or \\"",{source},"\\" has '
207        'unexpected contents."));').format(
208            target=target_expr,
209            source=source_expr,
210            code=common.ErrorCode.BAD_PATCH_FILE)))
211
212  def CacheFreeSpaceCheck(self, amount):
213    """Check that there's at least 'amount' space that can be made
214    available on /cache."""
215    self._required_cache = max(self._required_cache, amount)
216    self.script.append(('apply_patch_space(%d) || abort("E%d: Not enough free '
217                        'space on /cache to apply patches.");') % (
218                            amount,
219                            common.ErrorCode.INSUFFICIENT_CACHE_SPACE))
220
221  def Mount(self, mount_point, mount_options_by_format=""):
222    """Mount the partition with the given mount_point.
223      mount_options_by_format:
224      [fs_type=option[,option]...[|fs_type=option[,option]...]...]
225      where option is optname[=optvalue]
226      E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
227    """
228    fstab = self.fstab
229    if fstab:
230      p = fstab[mount_point]
231      mount_dict = {}
232      if mount_options_by_format is not None:
233        for option in mount_options_by_format.split("|"):
234          if "=" in option:
235            key, value = option.split("=", 1)
236            mount_dict[key] = value
237      mount_flags = mount_dict.get(p.fs_type, "")
238      if p.context is not None:
239        mount_flags = p.context + ("," + mount_flags if mount_flags else "")
240      self.script.append('mount("%s", "%s", %s, "%s", "%s");' % (
241          p.fs_type, common.PARTITION_TYPES[p.fs_type],
242          self._GetSlotSuffixDeviceForEntry(p),
243          p.mount_point, mount_flags))
244      self.mounts.add(p.mount_point)
245
246  def Comment(self, comment):
247    """Write a comment into the update script."""
248    self.script.append("")
249    for i in comment.split("\n"):
250      self.script.append("# " + i)
251    self.script.append("")
252
253  def Print(self, message):
254    """Log a message to the screen (if the logs are visible)."""
255    self.script.append('ui_print("%s");' % (message,))
256
257  def TunePartition(self, partition, *options):
258    fstab = self.fstab
259    if fstab:
260      p = fstab[partition]
261      if p.fs_type not in ("ext2", "ext3", "ext4"):
262        raise ValueError("Partition %s cannot be tuned\n" % (partition,))
263    self.script.append(
264        'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
265        '%s) || abort("E%d: Failed to tune partition %s");' % (
266            self._GetSlotSuffixDeviceForEntry(p),
267            common.ErrorCode.TUNE_PARTITION_FAILURE, partition))
268
269  def FormatPartition(self, partition):
270    """Format the given partition, specified by its mount point (eg,
271    "/system")."""
272
273    fstab = self.fstab
274    if fstab:
275      p = fstab[partition]
276      self.script.append('format("%s", "%s", %s, "%s", "%s");' %
277                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
278                          self._GetSlotSuffixDeviceForEntry(p),
279                          p.length, p.mount_point))
280
281  def WipeBlockDevice(self, partition):
282    if partition not in ("/system", "/vendor"):
283      raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
284    fstab = self.fstab
285    size = self.info.get(partition.lstrip("/") + "_size", None)
286    device = self._GetSlotSuffixDeviceForEntry(fstab[partition])
287
288    self.script.append('wipe_block_device(%s, %s);' % (device, size))
289
290  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
291    """Apply binary patches (in *patchpairs) to the given srcfile to
292    produce tgtfile (which may be "-" to indicate overwriting the
293    source file.
294
295    This edify function is being deprecated in favor of PatchPartition(). It
296    will try to redirect calls to PatchPartition() if possible. On unknown /
297    invalid inputs, raises an exception.
298    """
299    tokens = srcfile.split(':')
300    assert (len(tokens) == 6 and tokens[0] == 'EMMC' and tgtfile == '-' and
301            len(patchpairs) == 2), \
302        "Failed to handle unknown format. Use PatchPartition() instead."
303
304    # Also validity check the args.
305    assert tokens[3] == patchpairs[0], \
306        "Found mismatching values for source SHA-1: {} vs {}".format(
307            tokens[3], patchpairs[0])
308    assert int(tokens[4]) == tgtsize, \
309        "Found mismatching values for target size: {} vs {}".format(
310            tokens[4], tgtsize)
311    assert tokens[5] == tgtsha1, \
312        "Found mismatching values for target SHA-1: {} vs {}".format(
313            tokens[5], tgtsha1)
314
315    source = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[2], tokens[3])
316    target = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[4], tokens[5])
317    patch = patchpairs[1]
318    self.PatchPartition(target, source, patch)
319
320  def PatchPartition(self, target, source, patch):
321    """
322    Applies the patch to the source partition and writes it to target.
323
324    Args:
325      target: the target arg to patch_partition. Must be in the form of
326        foo:bar:baz:quux
327      source: the source arg to patch_partition. Must be in the form of
328        foo:bar:baz:quux
329      patch: the patch arg to patch_partition. Must be an unquoted string.
330    """
331    self._CheckSecondTokenNotSlotSuffixed(target, "PatchPartitionExpr")
332    self._CheckSecondTokenNotSlotSuffixed(source, "PatchPartitionExpr")
333    self.PatchPartitionExpr('"%s"' % target, '"%s"' % source, '"%s"' % patch)
334
335  def PatchPartitionExpr(self, target_expr, source_expr, patch_expr):
336    """
337    Applies the patch to the source partition and writes it to target.
338
339    Args:
340      target_expr: an Edify expression that serves as the target arg to
341        patch_partition. Must be evaluated to a string in the form of
342        foo:bar:baz:quux
343      source_expr: an Edify expression that serves as the source arg to
344        patch_partition. Must be evaluated to a string in the form of
345        foo:bar:baz:quux
346      patch_expr: an Edify expression that serves as the patch arg to
347        patch_partition. Must be evaluated to a string.
348    """
349    self.script.append(self.WordWrap((
350        'patch_partition({target},\0{source},\0'
351        'package_extract_file({patch})) ||\n'
352        '    abort(concat('
353        '        "E{code}: Failed to apply patch to ",{source}));').format(
354            target=target_expr,
355            source=source_expr,
356            patch=patch_expr,
357            code=common.ErrorCode.APPLY_PATCH_FAILURE)))
358
359  def _GetSlotSuffixDeviceForEntry(self, entry=None):
360    """
361    Args:
362      entry: the fstab entry of device "foo"
363    Returns:
364      An edify expression. Caller must not quote result.
365      If foo is slot suffixed, it returns
366        'add_slot_suffix("foo")'
367      Otherwise it returns
368        '"foo"' (quoted)
369    """
370    assert entry is not None
371    if entry.slotselect:
372      return 'add_slot_suffix("%s")' % entry.device
373    return '"%s"' % entry.device
374
375  def _CheckSecondTokenNotSlotSuffixed(self, s, fn):
376    lst = s.split(':')
377    assert(len(lst) == 4), "{} does not contain 4 tokens".format(s)
378    if self.fstab:
379      entry = common.GetEntryForDevice(self.fstab, lst[1])
380      if entry is not None:
381        assert not entry.slotselect, \
382          "Use %s because %s is slot suffixed" % (fn, lst[1])
383
384  def WriteRawImage(self, mount_point, fn, mapfn=None):
385    """Write the given package file into the partition for the given
386    mount point."""
387
388    fstab = self.fstab
389    if fstab:
390      p = fstab[mount_point]
391      partition_type = common.PARTITION_TYPES[p.fs_type]
392      device = self._GetSlotSuffixDeviceForEntry(p)
393      args = {'device': device, 'fn': fn}
394      if partition_type == "EMMC":
395        if mapfn:
396          args["map"] = mapfn
397          self.script.append(
398              'package_extract_file("%(fn)s", %(device)s, "%(map)s");' % args)
399        else:
400          self.script.append(
401              'package_extract_file("%(fn)s", %(device)s);' % args)
402      else:
403        raise ValueError(
404            "don't know how to write \"%s\" partitions" % p.fs_type)
405
406  def AppendExtra(self, extra):
407    """Append text verbatim to the output script."""
408    self.script.append(extra)
409
410  def Unmount(self, mount_point):
411    self.script.append('unmount("%s");' % mount_point)
412    self.mounts.remove(mount_point)
413
414  def UnmountAll(self):
415    for p in sorted(self.mounts):
416      self.script.append('unmount("%s");' % (p,))
417    self.mounts = set()
418
419  def AddToZip(self, input_zip, output_zip, input_path=None):
420    """Write the accumulated script to the output_zip file.  input_zip
421    is used as the source for the 'updater' binary needed to run
422    script.  If input_path is not None, it will be used as a local
423    path for the binary instead of input_zip."""
424
425    self.UnmountAll()
426
427    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
428                       "\n".join(self.script) + "\n")
429
430    if input_path is None:
431      data = input_zip.read("OTA/bin/updater")
432    else:
433      data = open(input_path, "rb").read()
434    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
435                       data, perms=0o755)
436