blob: cb0f6e6ba5cea7f3f485087ebcb38427e708dec4 [file] [log] [blame]
Kelvin Zhangcff4d762020-07-29 16:37:51 -04001# Copyright (C) 2020 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 copy
16import itertools
Yifan Hong125d0b62020-09-24 17:07:03 -070017import logging
Kelvin Zhangcff4d762020-07-29 16:37:51 -040018import os
19import zipfile
20
Tianjiea2076132020-08-19 17:25:32 -070021import ota_metadata_pb2
Kelvin Zhangcff4d762020-07-29 16:37:51 -040022from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
23 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
Yifan Hong125d0b62020-09-24 17:07:03 -070024 SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps,
25 MakeTempDir, RunAndCheckOutput, ExternalError)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040026
Yifan Hong125d0b62020-09-24 17:07:03 -070027logger = logging.getLogger(__name__)
Kelvin Zhang2e417382020-08-20 11:33:11 -040028
29OPTIONS.no_signing = False
30OPTIONS.force_non_ab = False
31OPTIONS.wipe_user_data = False
32OPTIONS.downgrade = False
33OPTIONS.key_passwords = {}
34OPTIONS.package_key = None
35OPTIONS.incremental_source = None
36OPTIONS.retrofit_dynamic_partitions = False
37OPTIONS.output_metadata_path = None
38OPTIONS.boot_variable_file = None
39
Kelvin Zhangcff4d762020-07-29 16:37:51 -040040METADATA_NAME = 'META-INF/com/android/metadata'
Tianjiea2076132020-08-19 17:25:32 -070041METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
Kelvin Zhangcff4d762020-07-29 16:37:51 -040042UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
43
Yifan Hong125d0b62020-09-24 17:07:03 -070044# See sysprop.mk. If file is moved, add new search paths here; don't remove
45# existing search paths.
46RAMDISK_BUILD_PROP_REL_PATHS = ['system/etc/ramdisk/build.prop']
Kelvin Zhangcff4d762020-07-29 16:37:51 -040047
48def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
49 """Finalizes the metadata and signs an A/B OTA package.
50
51 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
52 that contains the offsets and sizes for the ZIP entries. An example
53 property-files string is as follows.
54
55 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
56
57 OTA server can pass down this string, in addition to the package URL, to the
58 system update client. System update client can then fetch individual ZIP
59 entries (ZIP_STORED) directly at the given offset of the URL.
60
61 Args:
62 metadata: The metadata dict for the package.
63 input_file: The input ZIP filename that doesn't contain the package METADATA
64 entry yet.
65 output_file: The final output ZIP filename.
66 needed_property_files: The list of PropertyFiles' to be generated.
67 """
68
69 def ComputeAllPropertyFiles(input_file, needed_property_files):
70 # Write the current metadata entry with placeholders.
Kelvin Zhang928c2342020-09-22 16:15:57 -040071 with zipfile.ZipFile(input_file, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040072 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070073 metadata.property_files[property_files.name] = property_files.Compute(
74 input_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040075 namelist = input_zip.namelist()
76
Tianjiea2076132020-08-19 17:25:32 -070077 if METADATA_NAME in namelist or METADATA_PROTO_NAME in namelist:
78 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang928c2342020-09-22 16:15:57 -040079 output_zip = zipfile.ZipFile(input_file, 'a', allowZip64=True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040080 WriteMetadata(metadata, output_zip)
81 ZipClose(output_zip)
82
83 if OPTIONS.no_signing:
84 return input_file
85
86 prelim_signing = MakeTempFile(suffix='.zip')
87 SignOutput(input_file, prelim_signing)
88 return prelim_signing
89
90 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
Kelvin Zhang928c2342020-09-22 16:15:57 -040091 with zipfile.ZipFile(prelim_signing, allowZip64=True) as prelim_signing_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040092 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070093 metadata.property_files[property_files.name] = property_files.Finalize(
94 prelim_signing_zip,
95 len(metadata.property_files[property_files.name]))
Kelvin Zhangcff4d762020-07-29 16:37:51 -040096
97 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
98 # entries, as well as padding the entry headers. We do a preliminary signing
99 # (with an incomplete metadata entry) to allow that to happen. Then compute
100 # the ZIP entry offsets, write back the final metadata and do the final
101 # signing.
102 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
103 try:
104 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
105 except PropertyFiles.InsufficientSpaceException:
106 # Even with the preliminary signing, the entry orders may change
107 # dramatically, which leads to insufficiently reserved space during the
108 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
109 # preliminary signing works, based on the already ordered ZIP entries, to
110 # address the issue.
111 prelim_signing = ComputeAllPropertyFiles(
112 prelim_signing, needed_property_files)
113 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
114
115 # Replace the METADATA entry.
Tianjiea2076132020-08-19 17:25:32 -0700116 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang928c2342020-09-22 16:15:57 -0400117 output_zip = zipfile.ZipFile(prelim_signing, 'a', allowZip64=True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400118 WriteMetadata(metadata, output_zip)
119 ZipClose(output_zip)
120
121 # Re-sign the package after updating the metadata entry.
122 if OPTIONS.no_signing:
123 output_file = prelim_signing
124 else:
125 SignOutput(prelim_signing, output_file)
126
127 # Reopen the final signed zip to double check the streaming metadata.
Kelvin Zhang928c2342020-09-22 16:15:57 -0400128 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400129 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700130 property_files.Verify(
131 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400132
133 # If requested, dump the metadata to a separate file.
134 output_metadata_path = OPTIONS.output_metadata_path
135 if output_metadata_path:
136 WriteMetadata(metadata, output_metadata_path)
137
138
Tianjiea2076132020-08-19 17:25:32 -0700139def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400140 """Writes the metadata to the zip archive or a file.
141
142 Args:
Tianjiea2076132020-08-19 17:25:32 -0700143 metadata_proto: The metadata protobuf for the package.
144 output: A ZipFile object or a string of the output file path. If a string
145 path is given, the metadata in the protobuf format will be written to
146 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400147 """
148
Tianjiea2076132020-08-19 17:25:32 -0700149 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
150 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
151 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400152 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700153 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
154 compress_type=zipfile.ZIP_STORED)
155 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400156 compress_type=zipfile.ZIP_STORED)
157 return
158
Tianjiea2076132020-08-19 17:25:32 -0700159 with open('{}.pb'.format(output), 'w') as f:
160 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400161 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700162 f.write(legacy_metadata)
163
164
165def UpdateDeviceState(device_state, build_info, boot_variable_values,
166 is_post_build):
167 """Update the fields of the DeviceState proto with build info."""
168
Tianjie2bb14862020-08-28 16:24:34 -0700169 def UpdatePartitionStates(partition_states):
170 """Update the per-partition state according to its build.prop"""
Kelvin Zhang39aea442020-08-17 11:04:25 -0400171 if not build_info.is_ab:
172 return
Tianjie2bb14862020-08-28 16:24:34 -0700173 build_info_set = ComputeRuntimeBuildInfos(build_info,
174 boot_variable_values)
Kelvin Zhang39aea442020-08-17 11:04:25 -0400175 assert "ab_partitions" in build_info.info_dict,\
176 "ab_partitions property required for ab update."
177 ab_partitions = set(build_info.info_dict.get("ab_partitions"))
178
179 # delta_generator will error out on unused timestamps,
180 # so only generate timestamps for dynamic partitions
181 # used in OTA update.
182 for partition in sorted(set(PARTITIONS_WITH_CARE_MAP) & ab_partitions):
Tianjie2bb14862020-08-28 16:24:34 -0700183 partition_prop = build_info.info_dict.get(
184 '{}.build.prop'.format(partition))
185 # Skip if the partition is missing, or it doesn't have a build.prop
186 if not partition_prop or not partition_prop.build_props:
187 continue
188
189 partition_state = partition_states.add()
190 partition_state.partition_name = partition
191 # Update the partition's runtime device names and fingerprints
192 partition_devices = set()
193 partition_fingerprints = set()
194 for runtime_build_info in build_info_set:
195 partition_devices.add(
196 runtime_build_info.GetPartitionBuildProp('ro.product.device',
197 partition))
198 partition_fingerprints.add(
199 runtime_build_info.GetPartitionFingerprint(partition))
200
201 partition_state.device.extend(sorted(partition_devices))
202 partition_state.build.extend(sorted(partition_fingerprints))
203
204 # TODO(xunchang) set the boot image's version with kmi. Note the boot
205 # image doesn't have a file map.
206 partition_state.version = build_info.GetPartitionBuildProp(
207 'ro.build.date.utc', partition)
208
209 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos.
Tianjiea2076132020-08-19 17:25:32 -0700210 build_devices, build_fingerprints = \
211 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
212 device_state.device.extend(sorted(build_devices))
213 device_state.build.extend(sorted(build_fingerprints))
214 device_state.build_incremental = build_info.GetBuildProp(
215 'ro.build.version.incremental')
216
Tianjie2bb14862020-08-28 16:24:34 -0700217 UpdatePartitionStates(device_state.partition_state)
Tianjiea2076132020-08-19 17:25:32 -0700218
219 if is_post_build:
220 device_state.sdk_level = build_info.GetBuildProp(
221 'ro.build.version.sdk')
222 device_state.security_patch_level = build_info.GetBuildProp(
223 'ro.build.version.security_patch')
224 # Use the actual post-timestamp, even for a downgrade case.
225 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400226
227
228def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700229 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400230
Tianjiea2076132020-08-19 17:25:32 -0700231 It generates a ota_metadata protobuf that contains the info to be written
232 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
233 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400234
235 Args:
236 target_info: The BuildInfo instance that holds the target build info.
237 source_info: The BuildInfo instance that holds the source build info, or
238 None if generating full OTA.
239
240 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700241 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400242 """
243 assert isinstance(target_info, BuildInfo)
244 assert source_info is None or isinstance(source_info, BuildInfo)
245
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400246 boot_variable_values = {}
247 if OPTIONS.boot_variable_file:
248 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
249 for key, values in d.items():
250 boot_variable_values[key] = [val.strip() for val in values.split(',')]
251
Tianjiea2076132020-08-19 17:25:32 -0700252 metadata_proto = ota_metadata_pb2.OtaMetadata()
253 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
254 # consider skipping them if they aren't used by clients.
255 UpdateDeviceState(metadata_proto.postcondition, target_info,
256 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400257
258 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700259 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
260 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400261 else:
Tianjiea2076132020-08-19 17:25:32 -0700262 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
263 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400264
265 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700266 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400267
268 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700269 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400270
271 is_incremental = source_info is not None
272 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700273 UpdateDeviceState(metadata_proto.precondition, source_info,
274 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400275 else:
Tianjiea2076132020-08-19 17:25:32 -0700276 metadata_proto.precondition.device.extend(
277 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400278
279 # Detect downgrades and set up downgrade flags accordingly.
280 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700281 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400282
Tianjiea2076132020-08-19 17:25:32 -0700283 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400284
285
Tianjiea2076132020-08-19 17:25:32 -0700286def BuildLegacyOtaMetadata(metadata_proto):
287 """Converts the metadata proto to a legacy metadata dict.
288
289 This metadata dict is used to build the legacy metadata text file for
290 backward compatibility. We won't add new keys to the legacy metadata format.
291 If new information is needed, we should add it as a new field in OtaMetadata
292 proto definition.
293 """
294
295 separator = '|'
296
297 metadata_dict = {}
298 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
299 metadata_dict['ota-type'] = 'AB'
300 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
301 metadata_dict['ota-type'] = 'BLOCK'
302 if metadata_proto.wipe:
303 metadata_dict['ota-wipe'] = 'yes'
304 if metadata_proto.retrofit_dynamic_partitions:
305 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
306 if metadata_proto.downgrade:
307 metadata_dict['ota-downgrade'] = 'yes'
308
309 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
310
311 post_build = metadata_proto.postcondition
312 metadata_dict['post-build'] = separator.join(post_build.build)
313 metadata_dict['post-build-incremental'] = post_build.build_incremental
314 metadata_dict['post-sdk-level'] = post_build.sdk_level
315 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
316 metadata_dict['post-timestamp'] = str(post_build.timestamp)
317
318 pre_build = metadata_proto.precondition
319 metadata_dict['pre-device'] = separator.join(pre_build.device)
320 # incremental updates
321 if len(pre_build.build) != 0:
322 metadata_dict['pre-build'] = separator.join(pre_build.build)
323 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
324
325 metadata_dict.update(metadata_proto.property_files)
326
327 return metadata_dict
328
329
330def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400331 # Only incremental OTAs are allowed to reach here.
332 assert OPTIONS.incremental_source is not None
333
334 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
335 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
336 is_downgrade = int(post_timestamp) < int(pre_timestamp)
337
338 if OPTIONS.downgrade:
339 if not is_downgrade:
340 raise RuntimeError(
341 "--downgrade or --override_timestamp specified but no downgrade "
342 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700343 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400344 else:
345 if is_downgrade:
346 raise RuntimeError(
347 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
348 "Need to specify --override_timestamp OR --downgrade to allow "
349 "building the incremental." % (pre_timestamp, post_timestamp))
350
351
Tianjie2bb14862020-08-28 16:24:34 -0700352def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
353 """Returns a set of build info objects that may exist during runtime."""
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400354
Tianjie2bb14862020-08-28 16:24:34 -0700355 build_info_set = {default_build_info}
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400356 if not boot_variable_values:
Tianjie2bb14862020-08-28 16:24:34 -0700357 return build_info_set
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400358
359 # Calculate all possible combinations of the values for the boot variables.
360 keys = boot_variable_values.keys()
361 value_list = boot_variable_values.values()
362 combinations = [dict(zip(keys, values))
363 for values in itertools.product(*value_list)]
364 for placeholder_values in combinations:
365 # Reload the info_dict as some build properties may change their values
366 # based on the value of ro.boot* properties.
Tianjie2bb14862020-08-28 16:24:34 -0700367 info_dict = copy.deepcopy(default_build_info.info_dict)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400368 for partition in PARTITIONS_WITH_CARE_MAP:
369 partition_prop_key = "{}.build.prop".format(partition)
370 input_file = info_dict[partition_prop_key].input_file
371 if isinstance(input_file, zipfile.ZipFile):
Kelvin Zhang928c2342020-09-22 16:15:57 -0400372 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400373 info_dict[partition_prop_key] = \
374 PartitionBuildProps.FromInputFile(input_zip, partition,
375 placeholder_values)
376 else:
377 info_dict[partition_prop_key] = \
378 PartitionBuildProps.FromInputFile(input_file, partition,
379 placeholder_values)
380 info_dict["build.prop"] = info_dict["system.build.prop"]
Tianjie2bb14862020-08-28 16:24:34 -0700381 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400382
Tianjie2bb14862020-08-28 16:24:34 -0700383 return build_info_set
384
385
386def CalculateRuntimeDevicesAndFingerprints(default_build_info,
387 boot_variable_values):
388 """Returns a tuple of sets for runtime devices and fingerprints"""
389
390 device_names = set()
391 fingerprints = set()
392 build_info_set = ComputeRuntimeBuildInfos(default_build_info,
393 boot_variable_values)
394 for runtime_build_info in build_info_set:
395 device_names.add(runtime_build_info.device)
396 fingerprints.add(runtime_build_info.fingerprint)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400397 return device_names, fingerprints
398
399
400class PropertyFiles(object):
401 """A class that computes the property-files string for an OTA package.
402
403 A property-files string is a comma-separated string that contains the
404 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
405 can be fetched directly with the package URL along with the offset/size info.
406 These strings can be used for streaming A/B OTAs, or allowing an updater to
407 download package metadata entry directly, without paying the cost of
408 downloading entire package.
409
410 Computing the final property-files string requires two passes. Because doing
411 the whole package signing (with signapk.jar) will possibly reorder the ZIP
412 entries, which may in turn invalidate earlier computed ZIP entry offset/size
413 values.
414
415 This class provides functions to be called for each pass. The general flow is
416 as follows.
417
418 property_files = PropertyFiles()
419 # The first pass, which writes placeholders before doing initial signing.
420 property_files.Compute()
421 SignOutput()
422
423 # The second pass, by replacing the placeholders with actual data.
424 property_files.Finalize()
425 SignOutput()
426
427 And the caller can additionally verify the final result.
428
429 property_files.Verify()
430 """
431
432 def __init__(self):
433 self.name = None
434 self.required = ()
435 self.optional = ()
436
437 def Compute(self, input_zip):
438 """Computes and returns a property-files string with placeholders.
439
440 We reserve extra space for the offset and size of the metadata entry itself,
441 although we don't know the final values until the package gets signed.
442
443 Args:
444 input_zip: The input ZIP file.
445
446 Returns:
447 A string with placeholders for the metadata offset/size info, e.g.
448 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
449 """
450 return self.GetPropertyFilesString(input_zip, reserve_space=True)
451
452 class InsufficientSpaceException(Exception):
453 pass
454
455 def Finalize(self, input_zip, reserved_length):
456 """Finalizes a property-files string with actual METADATA offset/size info.
457
458 The input ZIP file has been signed, with the ZIP entries in the desired
459 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
460 the ZIP entry offsets and construct the property-files string with actual
461 data. Note that during this process, we must pad the property-files string
462 to the reserved length, so that the METADATA entry size remains the same.
463 Otherwise the entries' offsets and sizes may change again.
464
465 Args:
466 input_zip: The input ZIP file.
467 reserved_length: The reserved length of the property-files string during
468 the call to Compute(). The final string must be no more than this
469 size.
470
471 Returns:
472 A property-files string including the metadata offset/size info, e.g.
473 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
474
475 Raises:
476 InsufficientSpaceException: If the reserved length is insufficient to hold
477 the final string.
478 """
479 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
480 if len(result) > reserved_length:
481 raise self.InsufficientSpaceException(
482 'Insufficient reserved space: reserved={}, actual={}'.format(
483 reserved_length, len(result)))
484
485 result += ' ' * (reserved_length - len(result))
486 return result
487
488 def Verify(self, input_zip, expected):
489 """Verifies the input ZIP file contains the expected property-files string.
490
491 Args:
492 input_zip: The input ZIP file.
493 expected: The property-files string that's computed from Finalize().
494
495 Raises:
496 AssertionError: On finding a mismatch.
497 """
498 actual = self.GetPropertyFilesString(input_zip)
499 assert actual == expected, \
500 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
501
502 def GetPropertyFilesString(self, zip_file, reserve_space=False):
503 """
504 Constructs the property-files string per request.
505
506 Args:
507 zip_file: The input ZIP file.
508 reserved_length: The reserved length of the property-files string.
509
510 Returns:
511 A property-files string including the metadata offset/size info, e.g.
512 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
513 """
514
515 def ComputeEntryOffsetSize(name):
516 """Computes the zip entry offset and size."""
517 info = zip_file.getinfo(name)
518 offset = info.header_offset
519 offset += zipfile.sizeFileHeader
520 offset += len(info.extra) + len(info.filename)
521 size = info.file_size
522 return '%s:%d:%d' % (os.path.basename(name), offset, size)
523
524 tokens = []
525 tokens.extend(self._GetPrecomputed(zip_file))
526 for entry in self.required:
527 tokens.append(ComputeEntryOffsetSize(entry))
528 for entry in self.optional:
529 if entry in zip_file.namelist():
530 tokens.append(ComputeEntryOffsetSize(entry))
531
532 # 'META-INF/com/android/metadata' is required. We don't know its actual
533 # offset and length (as well as the values for other entries). So we reserve
534 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
535 # the space for metadata entry. Because 'offset' allows a max of 10-digit
536 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
537 # reserved space serves the metadata entry only.
538 if reserve_space:
539 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700540 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400541 else:
542 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Tianjiea2076132020-08-19 17:25:32 -0700543 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400544
545 return ','.join(tokens)
546
547 def _GetPrecomputed(self, input_zip):
548 """Computes the additional tokens to be included into the property-files.
549
550 This applies to tokens without actual ZIP entries, such as
551 payload_metadata.bin. We want to expose the offset/size to updaters, so
552 that they can download the payload metadata directly with the info.
553
554 Args:
555 input_zip: The input zip file.
556
557 Returns:
558 A list of strings (tokens) to be added to the property-files string.
559 """
560 # pylint: disable=no-self-use
561 # pylint: disable=unused-argument
562 return []
563
564
565def SignOutput(temp_zip_name, output_zip_name):
566 pw = OPTIONS.key_passwords[OPTIONS.package_key]
567
568 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
569 whole_file=True)
Yifan Hong125d0b62020-09-24 17:07:03 -0700570
571
572def GetBootImageTimestamp(boot_img):
573 """
574 Get timestamp from ramdisk within the boot image
575
576 Args:
577 boot_img: the boot image file. Ramdisk must be compressed with lz4 format.
578
579 Return:
580 An integer that corresponds to the timestamp of the boot image, or None
581 if file has unknown format. Raise exception if an unexpected error has
582 occurred.
583 """
584
585 tmp_dir = MakeTempDir('boot_', suffix='.img')
586 try:
587 RunAndCheckOutput(['unpack_bootimg', '--boot_img', boot_img, '--out', tmp_dir])
588 ramdisk = os.path.join(tmp_dir, 'ramdisk')
589 if not os.path.isfile(ramdisk):
590 logger.warning('Unable to get boot image timestamp: no ramdisk in boot')
591 return None
592 uncompressed_ramdisk = os.path.join(tmp_dir, 'uncompressed_ramdisk')
593 RunAndCheckOutput(['lz4', '-d', ramdisk, uncompressed_ramdisk])
594
595 abs_uncompressed_ramdisk = os.path.abspath(uncompressed_ramdisk)
596 extracted_ramdisk = MakeTempDir('extracted_ramdisk')
597 # Use "toybox cpio" instead of "cpio" because the latter invokes cpio from
598 # the host environment.
599 RunAndCheckOutput(['toybox', 'cpio', '-F', abs_uncompressed_ramdisk, '-i'],
600 cwd=extracted_ramdisk)
601
602 prop_file = None
603 for search_path in RAMDISK_BUILD_PROP_REL_PATHS:
604 prop_file = os.path.join(extracted_ramdisk, search_path)
605 if os.path.isfile(prop_file):
606 break
607 logger.warning('Unable to get boot image timestamp: no %s in ramdisk', search_path)
608
609 if not prop_file:
610 return None
611
612 props = PartitionBuildProps.FromBuildPropFile('boot', prop_file)
613 timestamp = props.GetProp('ro.bootimage.build.date.utc')
614 if timestamp:
615 return int(timestamp)
616 logger.warning('Unable to get boot image timestamp: ro.bootimage.build.date.utc is undefined')
617 return None
618
619 except ExternalError as e:
620 logger.warning('Unable to get boot image timestamp: %s', e)
621 return None