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