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 os
16import re
17
18import common
19
20class EdifyGenerator(object):
21  """Class to generate scripts in the 'edify' recovery script language
22  used from donut onwards."""
23
24  def __init__(self, version, info):
25    self.script = []
26    self.mounts = set()
27    self.version = version
28    self.info = info
29
30  def MakeTemporary(self):
31    """Make a temporary script object whose commands can latter be
32    appended to the parent script with AppendScript().  Used when the
33    caller wants to generate script commands out-of-order."""
34    x = EdifyGenerator(self.version, self.info)
35    x.mounts = self.mounts
36    return x
37
38  @staticmethod
39  def _WordWrap(cmd, linelen=80):
40    """'cmd' should be a function call with null characters after each
41    parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
42    to a given line length, replacing nulls with spaces and/or newlines
43    to format it nicely."""
44    indent = cmd.index("(")+1
45    out = []
46    first = True
47    x = re.compile("^(.{,%d})\0" % (linelen-indent,))
48    while True:
49      if not first:
50        out.append(" " * indent)
51      first = False
52      m = x.search(cmd)
53      if not m:
54        parts = cmd.split("\0", 1)
55        out.append(parts[0]+"\n")
56        if len(parts) == 1:
57          break
58        else:
59          cmd = parts[1]
60          continue
61      out.append(m.group(1)+"\n")
62      cmd = cmd[m.end():]
63
64    return "".join(out).replace("\0", " ").rstrip("\n")
65
66  def AppendScript(self, other):
67    """Append the contents of another script (which should be created
68    with temporary=True) to this one."""
69    self.script.extend(other.script)
70
71  def AssertOemProperty(self, name, value):
72    """Assert that a property on the OEM paritition matches a value."""
73    if not name:
74      raise ValueError("must specify an OEM property")
75    if not value:
76      raise ValueError("must specify the OEM value")
77    cmd = ('file_getprop("/oem/oem.prop", "%s") == "%s" || '
78           'abort("This package expects the value \\"%s\\"  for '
79           '\\"%s\\" on the OEM partition; '
80           'this has value \\"" + file_getprop("/oem/oem.prop") + "\\".");'
81           ) % (name, value, name, value)
82    self.script.append(cmd)
83
84  def AssertSomeFingerprint(self, *fp):
85    """Assert that the current recovery build fingerprint is one of *fp."""
86    if not fp:
87      raise ValueError("must specify some fingerprints")
88    cmd = (
89           ' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"')
90                        % i for i in fp]) +
91           ' ||\n    abort("Package expects build fingerprint of %s; this '
92           'device has " + getprop("ro.build.fingerprint") + ".");'
93           ) % (" or ".join(fp),)
94    self.script.append(cmd)
95
96  def AssertSomeThumbprint(self, *fp):
97    """Assert that the current recovery build thumbprint is one of *fp."""
98    if not fp:
99      raise ValueError("must specify some thumbprints")
100    cmd = (
101           ' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"')
102                        % i for i in fp]) +
103           ' ||\n    abort("Package expects build thumbprint of %s; this '
104           'device has " + getprop("ro.build.thumbprint") + ".");'
105           ) % (" or ".join(fp),)
106    self.script.append(cmd)
107
108  def AssertOlderBuild(self, timestamp, timestamp_text):
109    """Assert that the build on the device is older (or the same as)
110    the given timestamp."""
111    self.script.append(
112        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
113         'abort("Can\'t install this package (%s) over newer '
114         'build (" + getprop("ro.build.date") + ").");'
115         ) % (timestamp, timestamp_text))
116
117  def AssertDevice(self, device):
118    """Assert that the device identifier is the given string."""
119    cmd = ('getprop("ro.product.device") == "%s" || '
120           'abort("This package is for \\"%s\\" devices; '
121           'this is a \\"" + getprop("ro.product.device") + "\\".");'
122           ) % (device, device)
123    self.script.append(cmd)
124
125  def AssertSomeBootloader(self, *bootloaders):
126    """Asert that the bootloader version is one of *bootloaders."""
127    cmd = ("assert(" +
128           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
129                         for b in bootloaders]) +
130           ");")
131    self.script.append(self._WordWrap(cmd))
132
133  def ShowProgress(self, frac, dur):
134    """Update the progress bar, advancing it over 'frac' over the next
135    'dur' seconds.  'dur' may be zero to advance it via SetProgress
136    commands instead of by time."""
137    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
138
139  def SetProgress(self, frac):
140    """Set the position of the progress bar within the chunk defined
141    by the most recent ShowProgress call.  'frac' should be in
142    [0,1]."""
143    self.script.append("set_progress(%f);" % (frac,))
144
145  def PatchCheck(self, filename, *sha1):
146    """Check that the given file (or MTD reference) has one of the
147    given *sha1 hashes, checking the version saved in cache if the
148    file does not match."""
149    self.script.append(
150        'apply_patch_check("%s"' % (filename,) +
151        "".join([', "%s"' % (i,) for i in sha1]) +
152        ') || abort("\\"%s\\" has unexpected contents.");' % (filename,))
153
154  def FileCheck(self, filename, *sha1):
155    """Check that the given file (or MTD reference) has one of the
156    given *sha1 hashes."""
157    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
158                       "".join([', "%s"' % (i,) for i in sha1]) +
159                       '));')
160
161  def CacheFreeSpaceCheck(self, amount):
162    """Check that there's at least 'amount' space that can be made
163    available on /cache."""
164    self.script.append(('apply_patch_space(%d) || abort("Not enough free space '
165                        'on /system to apply patches.");') % (amount,))
166
167  def Mount(self, mount_point, mount_options_by_format=""):
168    """Mount the partition with the given mount_point.
169      mount_options_by_format:
170      [fs_type=option[,option]...[|fs_type=option[,option]...]...]
171      where option is optname[=optvalue]
172      E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
173    """
174    fstab = self.info.get("fstab", None)
175    if fstab:
176      p = fstab[mount_point]
177      mount_dict = {}
178      if mount_options_by_format is not None:
179        for option in mount_options_by_format.split("|"):
180          if "=" in option:
181            key, value = option.split("=", 1)
182            mount_dict[key] = value
183      self.script.append('mount("%s", "%s", "%s", "%s", "%s");' %
184                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
185                          p.device, p.mount_point, mount_dict.get(p.fs_type, "")))
186      self.mounts.add(p.mount_point)
187
188  def UnpackPackageDir(self, src, dst):
189    """Unpack a given directory from the OTA package into the given
190    destination directory."""
191    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
192
193  def Comment(self, comment):
194    """Write a comment into the update script."""
195    self.script.append("")
196    for i in comment.split("\n"):
197      self.script.append("# " + i)
198    self.script.append("")
199
200  def Print(self, message):
201    """Log a message to the screen (if the logs are visible)."""
202    self.script.append('ui_print("%s");' % (message,))
203
204  def FormatPartition(self, partition):
205    """Format the given partition, specified by its mount point (eg,
206    "/system")."""
207
208    reserve_size = 0
209    fstab = self.info.get("fstab", None)
210    if fstab:
211      p = fstab[partition]
212      self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
213                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
214                          p.device, p.length, p.mount_point))
215
216  def WipeBlockDevice(self, partition):
217    if partition not in ("/system", "/vendor"):
218      raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
219    fstab = self.info.get("fstab", None)
220    size = self.info.get(partition.lstrip("/") + "_size", None)
221    device = fstab[partition].device
222
223    self.script.append('wipe_block_device("%s", %s);' % (device, size))
224
225  def DeleteFiles(self, file_list):
226    """Delete all files in file_list."""
227    if not file_list: return
228    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
229    self.script.append(self._WordWrap(cmd))
230
231  def RenameFile(self, srcfile, tgtfile):
232    """Moves a file from one location to another."""
233    if self.info.get("update_rename_support", False):
234      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
235    else:
236      raise ValueError("Rename not supported by update binary")
237
238  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
239    """Prepend an action with an apply_patch_check in order to
240       skip the action if the file exists.  Used when a patch
241       is later renamed."""
242    cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1))
243    self.script.append(self._WordWrap(cmd))
244
245  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
246    """Apply binary patches (in *patchpairs) to the given srcfile to
247    produce tgtfile (which may be "-" to indicate overwriting the
248    source file."""
249    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
250      raise ValueError("bad patches given to ApplyPatch")
251    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
252           % (srcfile, tgtfile, tgtsha1, tgtsize)]
253    for i in range(0, len(patchpairs), 2):
254      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
255    cmd.append(');')
256    cmd = "".join(cmd)
257    self.script.append(self._WordWrap(cmd))
258
259  def WriteRawImage(self, mount_point, fn, mapfn=None):
260    """Write the given package file into the partition for the given
261    mount point."""
262
263    fstab = self.info["fstab"]
264    if fstab:
265      p = fstab[mount_point]
266      partition_type = common.PARTITION_TYPES[p.fs_type]
267      args = {'device': p.device, 'fn': fn}
268      if partition_type == "MTD":
269        self.script.append(
270            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
271            % args)
272      elif partition_type == "EMMC":
273        if mapfn:
274          args["map"] = mapfn
275          self.script.append(
276              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
277        else:
278          self.script.append(
279              'package_extract_file("%(fn)s", "%(device)s");' % args)
280      else:
281        raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,))
282
283  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
284    """Set file ownership and permissions."""
285    if not self.info.get("use_set_metadata", False):
286      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
287    else:
288      if capabilities is None: capabilities = "0x0"
289      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
290          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
291      if selabel is not None:
292        cmd += ', "selabel", "%s"' % ( selabel )
293      cmd += ');'
294      self.script.append(cmd)
295
296  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities):
297    """Recursively set path ownership and permissions."""
298    if not self.info.get("use_set_metadata", False):
299      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
300                         % (uid, gid, dmode, fmode, fn))
301    else:
302      if capabilities is None: capabilities = "0x0"
303      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
304          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
305          % (fn, uid, gid, dmode, fmode, capabilities)
306      if selabel is not None:
307        cmd += ', "selabel", "%s"' % ( selabel )
308      cmd += ');'
309      self.script.append(cmd)
310
311  def MakeSymlinks(self, symlink_list):
312    """Create symlinks, given a list of (dest, link) pairs."""
313    by_dest = {}
314    for d, l in symlink_list:
315      by_dest.setdefault(d, []).append(l)
316
317    for dest, links in sorted(by_dest.iteritems()):
318      cmd = ('symlink("%s", ' % (dest,) +
319             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
320      self.script.append(self._WordWrap(cmd))
321
322  def AppendExtra(self, extra):
323    """Append text verbatim to the output script."""
324    self.script.append(extra)
325
326  def Unmount(self, mount_point):
327    self.script.append('unmount("%s");' % (mount_point,))
328    self.mounts.remove(mount_point);
329
330  def UnmountAll(self):
331    for p in sorted(self.mounts):
332      self.script.append('unmount("%s");' % (p,))
333    self.mounts = set()
334
335  def AddToZip(self, input_zip, output_zip, input_path=None):
336    """Write the accumulated script to the output_zip file.  input_zip
337    is used as the source for the 'updater' binary needed to run
338    script.  If input_path is not None, it will be used as a local
339    path for the binary instead of input_zip."""
340
341    self.UnmountAll()
342
343    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
344                       "\n".join(self.script) + "\n")
345
346    if input_path is None:
347      data = input_zip.read("OTA/bin/updater")
348    else:
349      data = open(input_path, "rb").read()
350    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
351                       data, perms=0755)
352