blob: 874ab951c13cf3e54b339b8f04cd6e77872659b9 [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
17import os
18import zipfile
19
20from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
21 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
22 SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps)
23
24METADATA_NAME = 'META-INF/com/android/metadata'
25UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
26
27
28def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
29 """Finalizes the metadata and signs an A/B OTA package.
30
31 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
32 that contains the offsets and sizes for the ZIP entries. An example
33 property-files string is as follows.
34
35 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
36
37 OTA server can pass down this string, in addition to the package URL, to the
38 system update client. System update client can then fetch individual ZIP
39 entries (ZIP_STORED) directly at the given offset of the URL.
40
41 Args:
42 metadata: The metadata dict for the package.
43 input_file: The input ZIP filename that doesn't contain the package METADATA
44 entry yet.
45 output_file: The final output ZIP filename.
46 needed_property_files: The list of PropertyFiles' to be generated.
47 """
48
49 def ComputeAllPropertyFiles(input_file, needed_property_files):
50 # Write the current metadata entry with placeholders.
51 with zipfile.ZipFile(input_file) as input_zip:
52 for property_files in needed_property_files:
53 metadata[property_files.name] = property_files.Compute(input_zip)
54 namelist = input_zip.namelist()
55
56 if METADATA_NAME in namelist:
57 ZipDelete(input_file, METADATA_NAME)
58 output_zip = zipfile.ZipFile(input_file, 'a')
59 WriteMetadata(metadata, output_zip)
60 ZipClose(output_zip)
61
62 if OPTIONS.no_signing:
63 return input_file
64
65 prelim_signing = MakeTempFile(suffix='.zip')
66 SignOutput(input_file, prelim_signing)
67 return prelim_signing
68
69 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
70 with zipfile.ZipFile(prelim_signing) as prelim_signing_zip:
71 for property_files in needed_property_files:
72 metadata[property_files.name] = property_files.Finalize(
73 prelim_signing_zip, len(metadata[property_files.name]))
74
75 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
76 # entries, as well as padding the entry headers. We do a preliminary signing
77 # (with an incomplete metadata entry) to allow that to happen. Then compute
78 # the ZIP entry offsets, write back the final metadata and do the final
79 # signing.
80 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
81 try:
82 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
83 except PropertyFiles.InsufficientSpaceException:
84 # Even with the preliminary signing, the entry orders may change
85 # dramatically, which leads to insufficiently reserved space during the
86 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
87 # preliminary signing works, based on the already ordered ZIP entries, to
88 # address the issue.
89 prelim_signing = ComputeAllPropertyFiles(
90 prelim_signing, needed_property_files)
91 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
92
93 # Replace the METADATA entry.
94 ZipDelete(prelim_signing, METADATA_NAME)
95 output_zip = zipfile.ZipFile(prelim_signing, 'a')
96 WriteMetadata(metadata, output_zip)
97 ZipClose(output_zip)
98
99 # Re-sign the package after updating the metadata entry.
100 if OPTIONS.no_signing:
101 output_file = prelim_signing
102 else:
103 SignOutput(prelim_signing, output_file)
104
105 # Reopen the final signed zip to double check the streaming metadata.
106 with zipfile.ZipFile(output_file) as output_zip:
107 for property_files in needed_property_files:
108 property_files.Verify(output_zip, metadata[property_files.name].strip())
109
110 # If requested, dump the metadata to a separate file.
111 output_metadata_path = OPTIONS.output_metadata_path
112 if output_metadata_path:
113 WriteMetadata(metadata, output_metadata_path)
114
115
116def WriteMetadata(metadata, output):
117 """Writes the metadata to the zip archive or a file.
118
119 Args:
120 metadata: The metadata dict for the package.
121 output: A ZipFile object or a string of the output file path.
122 """
123
124 value = "".join(["%s=%s\n" % kv for kv in sorted(metadata.items())])
125 if isinstance(output, zipfile.ZipFile):
126 ZipWriteStr(output, METADATA_NAME, value,
127 compress_type=zipfile.ZIP_STORED)
128 return
129
130 with open(output, 'w') as f:
131 f.write(value)
132
133
134def GetPackageMetadata(target_info, source_info=None):
135 """Generates and returns the metadata dict.
136
137 It generates a dict() that contains the info to be written into an OTA
138 package (META-INF/com/android/metadata). It also handles the detection of
139 downgrade / data wipe based on the global options.
140
141 Args:
142 target_info: The BuildInfo instance that holds the target build info.
143 source_info: The BuildInfo instance that holds the source build info, or
144 None if generating full OTA.
145
146 Returns:
147 A dict to be written into package metadata entry.
148 """
149 assert isinstance(target_info, BuildInfo)
150 assert source_info is None or isinstance(source_info, BuildInfo)
151
152 separator = '|'
153
154 boot_variable_values = {}
155 if OPTIONS.boot_variable_file:
156 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
157 for key, values in d.items():
158 boot_variable_values[key] = [val.strip() for val in values.split(',')]
159
160 post_build_devices, post_build_fingerprints = \
161 CalculateRuntimeDevicesAndFingerprints(target_info, boot_variable_values)
162 metadata = {
163 'post-build': separator.join(sorted(post_build_fingerprints)),
164 'post-build-incremental': target_info.GetBuildProp(
165 'ro.build.version.incremental'),
166 'post-sdk-level': target_info.GetBuildProp(
167 'ro.build.version.sdk'),
168 'post-security-patch-level': target_info.GetBuildProp(
169 'ro.build.version.security_patch'),
170 }
171
172 if target_info.is_ab and not OPTIONS.force_non_ab:
173 metadata['ota-type'] = 'AB'
174 metadata['ota-required-cache'] = '0'
175 else:
176 metadata['ota-type'] = 'BLOCK'
177
178 if OPTIONS.wipe_user_data:
179 metadata['ota-wipe'] = 'yes'
180
181 if OPTIONS.retrofit_dynamic_partitions:
182 metadata['ota-retrofit-dynamic-partitions'] = 'yes'
183
184 is_incremental = source_info is not None
185 if is_incremental:
186 pre_build_devices, pre_build_fingerprints = \
187 CalculateRuntimeDevicesAndFingerprints(source_info,
188 boot_variable_values)
189 metadata['pre-build'] = separator.join(sorted(pre_build_fingerprints))
190 metadata['pre-build-incremental'] = source_info.GetBuildProp(
191 'ro.build.version.incremental')
192 metadata['pre-device'] = separator.join(sorted(pre_build_devices))
193 else:
194 metadata['pre-device'] = separator.join(sorted(post_build_devices))
195
196 # Use the actual post-timestamp, even for a downgrade case.
197 metadata['post-timestamp'] = target_info.GetBuildProp('ro.build.date.utc')
198
199 # Detect downgrades and set up downgrade flags accordingly.
200 if is_incremental:
201 HandleDowngradeMetadata(metadata, target_info, source_info)
202
203 return metadata
204
205
206def HandleDowngradeMetadata(metadata, target_info, source_info):
207 # Only incremental OTAs are allowed to reach here.
208 assert OPTIONS.incremental_source is not None
209
210 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
211 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
212 is_downgrade = int(post_timestamp) < int(pre_timestamp)
213
214 if OPTIONS.downgrade:
215 if not is_downgrade:
216 raise RuntimeError(
217 "--downgrade or --override_timestamp specified but no downgrade "
218 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
219 metadata["ota-downgrade"] = "yes"
220 else:
221 if is_downgrade:
222 raise RuntimeError(
223 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
224 "Need to specify --override_timestamp OR --downgrade to allow "
225 "building the incremental." % (pre_timestamp, post_timestamp))
226
227
228def CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values):
229 """Returns a tuple of sets for runtime devices and fingerprints"""
230
231 device_names = {build_info.device}
232 fingerprints = {build_info.fingerprint}
233
234 if not boot_variable_values:
235 return device_names, fingerprints
236
237 # Calculate all possible combinations of the values for the boot variables.
238 keys = boot_variable_values.keys()
239 value_list = boot_variable_values.values()
240 combinations = [dict(zip(keys, values))
241 for values in itertools.product(*value_list)]
242 for placeholder_values in combinations:
243 # Reload the info_dict as some build properties may change their values
244 # based on the value of ro.boot* properties.
245 info_dict = copy.deepcopy(build_info.info_dict)
246 for partition in PARTITIONS_WITH_CARE_MAP:
247 partition_prop_key = "{}.build.prop".format(partition)
248 input_file = info_dict[partition_prop_key].input_file
249 if isinstance(input_file, zipfile.ZipFile):
250 with zipfile.ZipFile(input_file.filename) as input_zip:
251 info_dict[partition_prop_key] = \
252 PartitionBuildProps.FromInputFile(input_zip, partition,
253 placeholder_values)
254 else:
255 info_dict[partition_prop_key] = \
256 PartitionBuildProps.FromInputFile(input_file, partition,
257 placeholder_values)
258 info_dict["build.prop"] = info_dict["system.build.prop"]
259
260 new_build_info = BuildInfo(info_dict, build_info.oem_dicts)
261 device_names.add(new_build_info.device)
262 fingerprints.add(new_build_info.fingerprint)
263 return device_names, fingerprints
264
265
266class PropertyFiles(object):
267 """A class that computes the property-files string for an OTA package.
268
269 A property-files string is a comma-separated string that contains the
270 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
271 can be fetched directly with the package URL along with the offset/size info.
272 These strings can be used for streaming A/B OTAs, or allowing an updater to
273 download package metadata entry directly, without paying the cost of
274 downloading entire package.
275
276 Computing the final property-files string requires two passes. Because doing
277 the whole package signing (with signapk.jar) will possibly reorder the ZIP
278 entries, which may in turn invalidate earlier computed ZIP entry offset/size
279 values.
280
281 This class provides functions to be called for each pass. The general flow is
282 as follows.
283
284 property_files = PropertyFiles()
285 # The first pass, which writes placeholders before doing initial signing.
286 property_files.Compute()
287 SignOutput()
288
289 # The second pass, by replacing the placeholders with actual data.
290 property_files.Finalize()
291 SignOutput()
292
293 And the caller can additionally verify the final result.
294
295 property_files.Verify()
296 """
297
298 def __init__(self):
299 self.name = None
300 self.required = ()
301 self.optional = ()
302
303 def Compute(self, input_zip):
304 """Computes and returns a property-files string with placeholders.
305
306 We reserve extra space for the offset and size of the metadata entry itself,
307 although we don't know the final values until the package gets signed.
308
309 Args:
310 input_zip: The input ZIP file.
311
312 Returns:
313 A string with placeholders for the metadata offset/size info, e.g.
314 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
315 """
316 return self.GetPropertyFilesString(input_zip, reserve_space=True)
317
318 class InsufficientSpaceException(Exception):
319 pass
320
321 def Finalize(self, input_zip, reserved_length):
322 """Finalizes a property-files string with actual METADATA offset/size info.
323
324 The input ZIP file has been signed, with the ZIP entries in the desired
325 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
326 the ZIP entry offsets and construct the property-files string with actual
327 data. Note that during this process, we must pad the property-files string
328 to the reserved length, so that the METADATA entry size remains the same.
329 Otherwise the entries' offsets and sizes may change again.
330
331 Args:
332 input_zip: The input ZIP file.
333 reserved_length: The reserved length of the property-files string during
334 the call to Compute(). The final string must be no more than this
335 size.
336
337 Returns:
338 A property-files string including the metadata offset/size info, e.g.
339 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
340
341 Raises:
342 InsufficientSpaceException: If the reserved length is insufficient to hold
343 the final string.
344 """
345 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
346 if len(result) > reserved_length:
347 raise self.InsufficientSpaceException(
348 'Insufficient reserved space: reserved={}, actual={}'.format(
349 reserved_length, len(result)))
350
351 result += ' ' * (reserved_length - len(result))
352 return result
353
354 def Verify(self, input_zip, expected):
355 """Verifies the input ZIP file contains the expected property-files string.
356
357 Args:
358 input_zip: The input ZIP file.
359 expected: The property-files string that's computed from Finalize().
360
361 Raises:
362 AssertionError: On finding a mismatch.
363 """
364 actual = self.GetPropertyFilesString(input_zip)
365 assert actual == expected, \
366 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
367
368 def GetPropertyFilesString(self, zip_file, reserve_space=False):
369 """
370 Constructs the property-files string per request.
371
372 Args:
373 zip_file: The input ZIP file.
374 reserved_length: The reserved length of the property-files string.
375
376 Returns:
377 A property-files string including the metadata offset/size info, e.g.
378 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
379 """
380
381 def ComputeEntryOffsetSize(name):
382 """Computes the zip entry offset and size."""
383 info = zip_file.getinfo(name)
384 offset = info.header_offset
385 offset += zipfile.sizeFileHeader
386 offset += len(info.extra) + len(info.filename)
387 size = info.file_size
388 return '%s:%d:%d' % (os.path.basename(name), offset, size)
389
390 tokens = []
391 tokens.extend(self._GetPrecomputed(zip_file))
392 for entry in self.required:
393 tokens.append(ComputeEntryOffsetSize(entry))
394 for entry in self.optional:
395 if entry in zip_file.namelist():
396 tokens.append(ComputeEntryOffsetSize(entry))
397
398 # 'META-INF/com/android/metadata' is required. We don't know its actual
399 # offset and length (as well as the values for other entries). So we reserve
400 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
401 # the space for metadata entry. Because 'offset' allows a max of 10-digit
402 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
403 # reserved space serves the metadata entry only.
404 if reserve_space:
405 tokens.append('metadata:' + ' ' * 15)
406 else:
407 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
408
409 return ','.join(tokens)
410
411 def _GetPrecomputed(self, input_zip):
412 """Computes the additional tokens to be included into the property-files.
413
414 This applies to tokens without actual ZIP entries, such as
415 payload_metadata.bin. We want to expose the offset/size to updaters, so
416 that they can download the payload metadata directly with the info.
417
418 Args:
419 input_zip: The input zip file.
420
421 Returns:
422 A list of strings (tokens) to be added to the property-files string.
423 """
424 # pylint: disable=no-self-use
425 # pylint: disable=unused-argument
426 return []
427
428
429def SignOutput(temp_zip_name, output_zip_name):
430 pw = OPTIONS.key_passwords[OPTIONS.package_key]
431
432 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
433 whole_file=True)