blob: c4195372c606da8ee966897d810a25a585bb3eac [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
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -070019import shutil
Kelvin Zhang25ab9982021-06-22 09:51:34 -040020import struct
Kelvin Zhangcff4d762020-07-29 16:37:51 -040021import zipfile
22
Tianjiea2076132020-08-19 17:25:32 -070023import ota_metadata_pb2
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000024import common
Kelvin Zhang37a42902022-10-26 12:49:03 -070025from common import (ZipDelete, OPTIONS, MakeTempFile,
Kelvin Zhangcff4d762020-07-29 16:37:51 -040026 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
TJ Rhoades6f488e92022-05-01 22:16:22 -070027 SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps,
Kelvin Zhangfcd731e2023-04-04 10:28:11 -070028 GetRamdiskFormat, ParseUpdateEngineConfig)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000029from payload_signer import PayloadSigner
30
Kelvin Zhangcff4d762020-07-29 16:37:51 -040031
Yifan Hong125d0b62020-09-24 17:07:03 -070032logger = logging.getLogger(__name__)
Kelvin Zhang2e417382020-08-20 11:33:11 -040033
34OPTIONS.no_signing = False
35OPTIONS.force_non_ab = False
36OPTIONS.wipe_user_data = False
37OPTIONS.downgrade = False
38OPTIONS.key_passwords = {}
39OPTIONS.package_key = None
40OPTIONS.incremental_source = None
41OPTIONS.retrofit_dynamic_partitions = False
42OPTIONS.output_metadata_path = None
43OPTIONS.boot_variable_file = None
44
Kelvin Zhangcff4d762020-07-29 16:37:51 -040045METADATA_NAME = 'META-INF/com/android/metadata'
Tianjiea2076132020-08-19 17:25:32 -070046METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
Kelvin Zhangcff4d762020-07-29 16:37:51 -040047UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
Kelvin Zhang05ff7052021-02-10 09:13:26 -050048SECURITY_PATCH_LEVEL_PROP_NAME = "ro.build.version.security_patch"
49
Kelvin Zhangcff4d762020-07-29 16:37:51 -040050
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000051def FinalizeMetadata(metadata, input_file, output_file, needed_property_files=None, package_key=None, pw=None):
Kelvin Zhangcff4d762020-07-29 16:37:51 -040052 """Finalizes the metadata and signs an A/B OTA package.
53
54 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
55 that contains the offsets and sizes for the ZIP entries. An example
56 property-files string is as follows.
57
58 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
59
60 OTA server can pass down this string, in addition to the package URL, to the
61 system update client. System update client can then fetch individual ZIP
62 entries (ZIP_STORED) directly at the given offset of the URL.
63
64 Args:
65 metadata: The metadata dict for the package.
66 input_file: The input ZIP filename that doesn't contain the package METADATA
67 entry yet.
68 output_file: The final output ZIP filename.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000069 needed_property_files: The list of PropertyFiles' to be generated. Default is [AbOtaPropertyFiles(), StreamingPropertyFiles()]
70 package_key: The key used to sign this OTA package
71 pw: Password for the package_key
Kelvin Zhangcff4d762020-07-29 16:37:51 -040072 """
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000073 no_signing = package_key is None
74
75 if needed_property_files is None:
76 # AbOtaPropertyFiles intends to replace StreamingPropertyFiles, as it covers
77 # all the info of the latter. However, system updaters and OTA servers need to
78 # take time to switch to the new flag. We keep both of the flags for
79 # P-timeframe, and will remove StreamingPropertyFiles in later release.
80 needed_property_files = (
81 AbOtaPropertyFiles(),
82 StreamingPropertyFiles(),
83 )
Kelvin Zhangcff4d762020-07-29 16:37:51 -040084
85 def ComputeAllPropertyFiles(input_file, needed_property_files):
86 # Write the current metadata entry with placeholders.
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -070087 with zipfile.ZipFile(input_file, 'r', allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040088 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070089 metadata.property_files[property_files.name] = property_files.Compute(
90 input_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040091
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -070092 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME], True)
93 with zipfile.ZipFile(input_file, 'a', allowZip64=True) as output_zip:
94 WriteMetadata(metadata, output_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040095
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000096 if no_signing:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040097 return input_file
98
99 prelim_signing = MakeTempFile(suffix='.zip')
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000100 SignOutput(input_file, prelim_signing, package_key, pw)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400101 return prelim_signing
102
103 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700104 with zipfile.ZipFile(prelim_signing, 'r', allowZip64=True) as prelim_signing_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400105 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700106 metadata.property_files[property_files.name] = property_files.Finalize(
107 prelim_signing_zip,
108 len(metadata.property_files[property_files.name]))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400109
110 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
111 # entries, as well as padding the entry headers. We do a preliminary signing
112 # (with an incomplete metadata entry) to allow that to happen. Then compute
113 # the ZIP entry offsets, write back the final metadata and do the final
114 # signing.
115 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
116 try:
117 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
118 except PropertyFiles.InsufficientSpaceException:
119 # Even with the preliminary signing, the entry orders may change
120 # dramatically, which leads to insufficiently reserved space during the
121 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
122 # preliminary signing works, based on the already ordered ZIP entries, to
123 # address the issue.
124 prelim_signing = ComputeAllPropertyFiles(
125 prelim_signing, needed_property_files)
126 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
127
128 # Replace the METADATA entry.
Tianjiea2076132020-08-19 17:25:32 -0700129 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700130 with zipfile.ZipFile(prelim_signing, 'a', allowZip64=True) as output_zip:
131 WriteMetadata(metadata, output_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400132
133 # Re-sign the package after updating the metadata entry.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000134 if no_signing:
Kelvin Zhangf80e8862023-01-20 10:18:11 -0800135 logger.info(f"Signing disabled for output file {output_file}")
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -0700136 shutil.copy(prelim_signing, output_file)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400137 else:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700138 logger.info(
139 f"Signing the output file {output_file} with key {package_key}")
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000140 SignOutput(prelim_signing, output_file, package_key, pw)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400141
142 # Reopen the final signed zip to double check the streaming metadata.
Kelvin Zhang928c2342020-09-22 16:15:57 -0400143 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400144 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700145 property_files.Verify(
146 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400147
148 # If requested, dump the metadata to a separate file.
149 output_metadata_path = OPTIONS.output_metadata_path
150 if output_metadata_path:
151 WriteMetadata(metadata, output_metadata_path)
152
153
Tianjiea2076132020-08-19 17:25:32 -0700154def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400155 """Writes the metadata to the zip archive or a file.
156
157 Args:
Tianjiea2076132020-08-19 17:25:32 -0700158 metadata_proto: The metadata protobuf for the package.
159 output: A ZipFile object or a string of the output file path. If a string
160 path is given, the metadata in the protobuf format will be written to
161 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400162 """
163
Tianjiea2076132020-08-19 17:25:32 -0700164 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
165 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
166 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400167 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700168 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
169 compress_type=zipfile.ZIP_STORED)
170 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400171 compress_type=zipfile.ZIP_STORED)
172 return
173
Cole Faustb820bcd2021-10-28 13:59:48 -0700174 with open('{}.pb'.format(output), 'wb') as f:
Tianjiea2076132020-08-19 17:25:32 -0700175 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400176 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700177 f.write(legacy_metadata)
178
179
180def UpdateDeviceState(device_state, build_info, boot_variable_values,
181 is_post_build):
182 """Update the fields of the DeviceState proto with build info."""
183
Tianjie2bb14862020-08-28 16:24:34 -0700184 def UpdatePartitionStates(partition_states):
185 """Update the per-partition state according to its build.prop"""
Kelvin Zhang39aea442020-08-17 11:04:25 -0400186 if not build_info.is_ab:
187 return
Tianjie2bb14862020-08-28 16:24:34 -0700188 build_info_set = ComputeRuntimeBuildInfos(build_info,
189 boot_variable_values)
Kelvin Zhang39aea442020-08-17 11:04:25 -0400190 assert "ab_partitions" in build_info.info_dict,\
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500191 "ab_partitions property required for ab update."
Kelvin Zhang39aea442020-08-17 11:04:25 -0400192 ab_partitions = set(build_info.info_dict.get("ab_partitions"))
193
194 # delta_generator will error out on unused timestamps,
195 # so only generate timestamps for dynamic partitions
196 # used in OTA update.
Yifan Hong5057b952021-01-07 14:09:57 -0800197 for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions):
Tianjie2bb14862020-08-28 16:24:34 -0700198 partition_prop = build_info.info_dict.get(
199 '{}.build.prop'.format(partition))
200 # Skip if the partition is missing, or it doesn't have a build.prop
201 if not partition_prop or not partition_prop.build_props:
202 continue
203
204 partition_state = partition_states.add()
205 partition_state.partition_name = partition
206 # Update the partition's runtime device names and fingerprints
207 partition_devices = set()
208 partition_fingerprints = set()
209 for runtime_build_info in build_info_set:
210 partition_devices.add(
211 runtime_build_info.GetPartitionBuildProp('ro.product.device',
212 partition))
213 partition_fingerprints.add(
214 runtime_build_info.GetPartitionFingerprint(partition))
215
216 partition_state.device.extend(sorted(partition_devices))
217 partition_state.build.extend(sorted(partition_fingerprints))
218
219 # TODO(xunchang) set the boot image's version with kmi. Note the boot
220 # image doesn't have a file map.
221 partition_state.version = build_info.GetPartitionBuildProp(
222 'ro.build.date.utc', partition)
223
224 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos.
Tianjiea2076132020-08-19 17:25:32 -0700225 build_devices, build_fingerprints = \
226 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
227 device_state.device.extend(sorted(build_devices))
228 device_state.build.extend(sorted(build_fingerprints))
229 device_state.build_incremental = build_info.GetBuildProp(
230 'ro.build.version.incremental')
231
Tianjie2bb14862020-08-28 16:24:34 -0700232 UpdatePartitionStates(device_state.partition_state)
Tianjiea2076132020-08-19 17:25:32 -0700233
234 if is_post_build:
235 device_state.sdk_level = build_info.GetBuildProp(
236 'ro.build.version.sdk')
237 device_state.security_patch_level = build_info.GetBuildProp(
238 'ro.build.version.security_patch')
239 # Use the actual post-timestamp, even for a downgrade case.
240 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400241
242
243def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700244 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400245
Tianjiea2076132020-08-19 17:25:32 -0700246 It generates a ota_metadata protobuf that contains the info to be written
247 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
248 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400249
250 Args:
251 target_info: The BuildInfo instance that holds the target build info.
252 source_info: The BuildInfo instance that holds the source build info, or
253 None if generating full OTA.
254
255 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700256 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400257 """
258 assert isinstance(target_info, BuildInfo)
259 assert source_info is None or isinstance(source_info, BuildInfo)
260
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400261 boot_variable_values = {}
262 if OPTIONS.boot_variable_file:
263 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
264 for key, values in d.items():
265 boot_variable_values[key] = [val.strip() for val in values.split(',')]
266
Tianjiea2076132020-08-19 17:25:32 -0700267 metadata_proto = ota_metadata_pb2.OtaMetadata()
268 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
269 # consider skipping them if they aren't used by clients.
270 UpdateDeviceState(metadata_proto.postcondition, target_info,
271 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400272
273 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700274 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
275 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400276 else:
Tianjiea2076132020-08-19 17:25:32 -0700277 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
278 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400279
280 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700281 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400282
283 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700284 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400285
286 is_incremental = source_info is not None
287 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700288 UpdateDeviceState(metadata_proto.precondition, source_info,
289 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400290 else:
Tianjiea2076132020-08-19 17:25:32 -0700291 metadata_proto.precondition.device.extend(
292 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400293
294 # Detect downgrades and set up downgrade flags accordingly.
295 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700296 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400297
Tianjiea2076132020-08-19 17:25:32 -0700298 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400299
300
Tianjiea2076132020-08-19 17:25:32 -0700301def BuildLegacyOtaMetadata(metadata_proto):
302 """Converts the metadata proto to a legacy metadata dict.
303
304 This metadata dict is used to build the legacy metadata text file for
305 backward compatibility. We won't add new keys to the legacy metadata format.
306 If new information is needed, we should add it as a new field in OtaMetadata
307 proto definition.
308 """
309
310 separator = '|'
311
312 metadata_dict = {}
313 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
314 metadata_dict['ota-type'] = 'AB'
315 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
316 metadata_dict['ota-type'] = 'BLOCK'
317 if metadata_proto.wipe:
318 metadata_dict['ota-wipe'] = 'yes'
319 if metadata_proto.retrofit_dynamic_partitions:
320 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
321 if metadata_proto.downgrade:
322 metadata_dict['ota-downgrade'] = 'yes'
323
324 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
325
326 post_build = metadata_proto.postcondition
327 metadata_dict['post-build'] = separator.join(post_build.build)
328 metadata_dict['post-build-incremental'] = post_build.build_incremental
329 metadata_dict['post-sdk-level'] = post_build.sdk_level
330 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
331 metadata_dict['post-timestamp'] = str(post_build.timestamp)
332
333 pre_build = metadata_proto.precondition
334 metadata_dict['pre-device'] = separator.join(pre_build.device)
335 # incremental updates
336 if len(pre_build.build) != 0:
337 metadata_dict['pre-build'] = separator.join(pre_build.build)
338 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
339
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500340 if metadata_proto.spl_downgrade:
341 metadata_dict['spl-downgrade'] = 'yes'
Tianjiea2076132020-08-19 17:25:32 -0700342 metadata_dict.update(metadata_proto.property_files)
343
344 return metadata_dict
345
346
347def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400348 # Only incremental OTAs are allowed to reach here.
349 assert OPTIONS.incremental_source is not None
350
351 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
352 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
353 is_downgrade = int(post_timestamp) < int(pre_timestamp)
354
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500355 if OPTIONS.spl_downgrade:
356 metadata_proto.spl_downgrade = True
357
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400358 if OPTIONS.downgrade:
359 if not is_downgrade:
360 raise RuntimeError(
361 "--downgrade or --override_timestamp specified but no downgrade "
362 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700363 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400364 else:
365 if is_downgrade:
366 raise RuntimeError(
367 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
368 "Need to specify --override_timestamp OR --downgrade to allow "
369 "building the incremental." % (pre_timestamp, post_timestamp))
370
371
Tianjie2bb14862020-08-28 16:24:34 -0700372def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
373 """Returns a set of build info objects that may exist during runtime."""
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400374
Tianjie2bb14862020-08-28 16:24:34 -0700375 build_info_set = {default_build_info}
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400376 if not boot_variable_values:
Tianjie2bb14862020-08-28 16:24:34 -0700377 return build_info_set
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400378
379 # Calculate all possible combinations of the values for the boot variables.
380 keys = boot_variable_values.keys()
381 value_list = boot_variable_values.values()
382 combinations = [dict(zip(keys, values))
383 for values in itertools.product(*value_list)]
384 for placeholder_values in combinations:
385 # Reload the info_dict as some build properties may change their values
386 # based on the value of ro.boot* properties.
Tianjie2bb14862020-08-28 16:24:34 -0700387 info_dict = copy.deepcopy(default_build_info.info_dict)
Yifan Hong5057b952021-01-07 14:09:57 -0800388 for partition in PARTITIONS_WITH_BUILD_PROP:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400389 partition_prop_key = "{}.build.prop".format(partition)
390 input_file = info_dict[partition_prop_key].input_file
TJ Rhoades6f488e92022-05-01 22:16:22 -0700391 ramdisk = GetRamdiskFormat(info_dict)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400392 if isinstance(input_file, zipfile.ZipFile):
Kelvin Zhang928c2342020-09-22 16:15:57 -0400393 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400394 info_dict[partition_prop_key] = \
395 PartitionBuildProps.FromInputFile(input_zip, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700396 placeholder_values,
397 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400398 else:
399 info_dict[partition_prop_key] = \
400 PartitionBuildProps.FromInputFile(input_file, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700401 placeholder_values,
402 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400403 info_dict["build.prop"] = info_dict["system.build.prop"]
Tianjie2bb14862020-08-28 16:24:34 -0700404 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400405
Tianjie2bb14862020-08-28 16:24:34 -0700406 return build_info_set
407
408
409def CalculateRuntimeDevicesAndFingerprints(default_build_info,
410 boot_variable_values):
411 """Returns a tuple of sets for runtime devices and fingerprints"""
412
413 device_names = set()
414 fingerprints = set()
415 build_info_set = ComputeRuntimeBuildInfos(default_build_info,
416 boot_variable_values)
417 for runtime_build_info in build_info_set:
418 device_names.add(runtime_build_info.device)
419 fingerprints.add(runtime_build_info.fingerprint)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400420 return device_names, fingerprints
421
422
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400423def GetZipEntryOffset(zfp, entry_info):
424 """Get offset to a beginning of a particular zip entry
425 Args:
426 fp: zipfile.ZipFile
427 entry_info: zipfile.ZipInfo
428
429 Returns:
430 (offset, size) tuple
431 """
432 # Don't use len(entry_info.extra). Because that returns size of extra
433 # fields in central directory. We need to look at local file directory,
434 # as these two might have different sizes.
435
436 # We cannot work with zipfile.ZipFile instances, we need a |fp| for the underlying file.
437 zfp = zfp.fp
438 zfp.seek(entry_info.header_offset)
439 data = zfp.read(zipfile.sizeFileHeader)
440 fheader = struct.unpack(zipfile.structFileHeader, data)
441 # Last two fields of local file header are filename length and
442 # extra length
443 filename_len = fheader[-2]
444 extra_len = fheader[-1]
445 offset = entry_info.header_offset
446 offset += zipfile.sizeFileHeader
447 offset += filename_len + extra_len
448 size = entry_info.file_size
449 return (offset, size)
450
451
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400452class PropertyFiles(object):
453 """A class that computes the property-files string for an OTA package.
454
455 A property-files string is a comma-separated string that contains the
456 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
457 can be fetched directly with the package URL along with the offset/size info.
458 These strings can be used for streaming A/B OTAs, or allowing an updater to
459 download package metadata entry directly, without paying the cost of
460 downloading entire package.
461
462 Computing the final property-files string requires two passes. Because doing
463 the whole package signing (with signapk.jar) will possibly reorder the ZIP
464 entries, which may in turn invalidate earlier computed ZIP entry offset/size
465 values.
466
467 This class provides functions to be called for each pass. The general flow is
468 as follows.
469
470 property_files = PropertyFiles()
471 # The first pass, which writes placeholders before doing initial signing.
472 property_files.Compute()
473 SignOutput()
474
475 # The second pass, by replacing the placeholders with actual data.
476 property_files.Finalize()
477 SignOutput()
478
479 And the caller can additionally verify the final result.
480
481 property_files.Verify()
482 """
483
484 def __init__(self):
485 self.name = None
486 self.required = ()
487 self.optional = ()
488
489 def Compute(self, input_zip):
490 """Computes and returns a property-files string with placeholders.
491
492 We reserve extra space for the offset and size of the metadata entry itself,
493 although we don't know the final values until the package gets signed.
494
495 Args:
496 input_zip: The input ZIP file.
497
498 Returns:
499 A string with placeholders for the metadata offset/size info, e.g.
500 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
501 """
502 return self.GetPropertyFilesString(input_zip, reserve_space=True)
503
504 class InsufficientSpaceException(Exception):
505 pass
506
507 def Finalize(self, input_zip, reserved_length):
508 """Finalizes a property-files string with actual METADATA offset/size info.
509
510 The input ZIP file has been signed, with the ZIP entries in the desired
511 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
512 the ZIP entry offsets and construct the property-files string with actual
513 data. Note that during this process, we must pad the property-files string
514 to the reserved length, so that the METADATA entry size remains the same.
515 Otherwise the entries' offsets and sizes may change again.
516
517 Args:
518 input_zip: The input ZIP file.
519 reserved_length: The reserved length of the property-files string during
520 the call to Compute(). The final string must be no more than this
521 size.
522
523 Returns:
524 A property-files string including the metadata offset/size info, e.g.
525 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
526
527 Raises:
528 InsufficientSpaceException: If the reserved length is insufficient to hold
529 the final string.
530 """
531 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
532 if len(result) > reserved_length:
533 raise self.InsufficientSpaceException(
534 'Insufficient reserved space: reserved={}, actual={}'.format(
535 reserved_length, len(result)))
536
537 result += ' ' * (reserved_length - len(result))
538 return result
539
540 def Verify(self, input_zip, expected):
541 """Verifies the input ZIP file contains the expected property-files string.
542
543 Args:
544 input_zip: The input ZIP file.
545 expected: The property-files string that's computed from Finalize().
546
547 Raises:
548 AssertionError: On finding a mismatch.
549 """
550 actual = self.GetPropertyFilesString(input_zip)
551 assert actual == expected, \
552 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
553
554 def GetPropertyFilesString(self, zip_file, reserve_space=False):
555 """
556 Constructs the property-files string per request.
557
558 Args:
559 zip_file: The input ZIP file.
560 reserved_length: The reserved length of the property-files string.
561
562 Returns:
563 A property-files string including the metadata offset/size info, e.g.
564 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
565 """
566
567 def ComputeEntryOffsetSize(name):
568 """Computes the zip entry offset and size."""
569 info = zip_file.getinfo(name)
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400570 (offset, size) = GetZipEntryOffset(zip_file, info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400571 return '%s:%d:%d' % (os.path.basename(name), offset, size)
572
573 tokens = []
574 tokens.extend(self._GetPrecomputed(zip_file))
575 for entry in self.required:
576 tokens.append(ComputeEntryOffsetSize(entry))
577 for entry in self.optional:
578 if entry in zip_file.namelist():
579 tokens.append(ComputeEntryOffsetSize(entry))
580
581 # 'META-INF/com/android/metadata' is required. We don't know its actual
582 # offset and length (as well as the values for other entries). So we reserve
583 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
584 # the space for metadata entry. Because 'offset' allows a max of 10-digit
585 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
586 # reserved space serves the metadata entry only.
587 if reserve_space:
588 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700589 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400590 else:
591 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Luca Stefanib6075c52021-11-03 17:10:54 +0100592 if METADATA_PROTO_NAME in zip_file.namelist():
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700593 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400594
595 return ','.join(tokens)
596
597 def _GetPrecomputed(self, input_zip):
598 """Computes the additional tokens to be included into the property-files.
599
600 This applies to tokens without actual ZIP entries, such as
601 payload_metadata.bin. We want to expose the offset/size to updaters, so
602 that they can download the payload metadata directly with the info.
603
604 Args:
605 input_zip: The input zip file.
606
607 Returns:
608 A list of strings (tokens) to be added to the property-files string.
609 """
610 # pylint: disable=no-self-use
611 # pylint: disable=unused-argument
612 return []
613
614
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000615def SignOutput(temp_zip_name, output_zip_name, package_key=None, pw=None):
616 if package_key is None:
617 package_key = OPTIONS.package_key
618 if pw is None and OPTIONS.key_passwords:
619 pw = OPTIONS.key_passwords[package_key]
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400620
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000621 SignFile(temp_zip_name, output_zip_name, package_key, pw,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400622 whole_file=True)
Tianjiea5fca032021-06-01 22:06:28 -0700623
624
625def ConstructOtaApexInfo(target_zip, source_file=None):
626 """If applicable, add the source version to the apex info."""
627
628 def _ReadApexInfo(input_zip):
629 if "META/apex_info.pb" not in input_zip.namelist():
630 logger.warning("target_file doesn't contain apex_info.pb %s", input_zip)
631 return None
632
633 with input_zip.open("META/apex_info.pb", "r") as zfp:
634 return zfp.read()
635
636 target_apex_string = _ReadApexInfo(target_zip)
637 # Return early if the target apex info doesn't exist or is empty.
638 if not target_apex_string:
639 return target_apex_string
640
641 # If the source apex info isn't available, just return the target info
642 if not source_file:
643 return target_apex_string
644
645 with zipfile.ZipFile(source_file, "r", allowZip64=True) as source_zip:
646 source_apex_string = _ReadApexInfo(source_zip)
647 if not source_apex_string:
648 return target_apex_string
649
650 source_apex_proto = ota_metadata_pb2.ApexMetadata()
651 source_apex_proto.ParseFromString(source_apex_string)
652 source_apex_versions = {apex.package_name: apex.version for apex in
653 source_apex_proto.apex_info}
654
655 # If the apex package is available in the source build, initialize the source
656 # apex version.
657 target_apex_proto = ota_metadata_pb2.ApexMetadata()
658 target_apex_proto.ParseFromString(target_apex_string)
659 for target_apex in target_apex_proto.apex_info:
660 name = target_apex.package_name
661 if name in source_apex_versions:
662 target_apex.source_version = source_apex_versions[name]
663
664 return target_apex_proto.SerializeToString()
Kelvin Zhang410bb382022-01-06 09:15:54 -0800665
666
Kelvin Zhangf2728d62022-01-10 11:42:36 -0800667def IsLz4diffCompatible(source_file: str, target_file: str):
668 """Check whether lz4diff versions in two builds are compatible
669
670 Args:
671 source_file: Path to source build's target_file.zip
672 target_file: Path to target build's target_file.zip
673
674 Returns:
675 bool true if and only if lz4diff versions are compatible
676 """
677 if source_file is None or target_file is None:
678 return False
679 # Right now we enable lz4diff as long as source build has liblz4.so.
680 # In the future we might introduce version system to lz4diff as well.
681 if zipfile.is_zipfile(source_file):
682 with zipfile.ZipFile(source_file, "r") as zfp:
683 return "META/liblz4.so" in zfp.namelist()
684 else:
685 assert os.path.isdir(source_file)
686 return os.path.exists(os.path.join(source_file, "META", "liblz4.so"))
687
688
Kelvin Zhang410bb382022-01-06 09:15:54 -0800689def IsZucchiniCompatible(source_file: str, target_file: str):
690 """Check whether zucchini versions in two builds are compatible
691
692 Args:
693 source_file: Path to source build's target_file.zip
694 target_file: Path to target build's target_file.zip
695
696 Returns:
697 bool true if and only if zucchini versions are compatible
698 """
699 if source_file is None or target_file is None:
700 return False
701 assert os.path.exists(source_file)
702 assert os.path.exists(target_file)
703
704 assert zipfile.is_zipfile(source_file) or os.path.isdir(source_file)
705 assert zipfile.is_zipfile(target_file) or os.path.isdir(target_file)
706 _ZUCCHINI_CONFIG_ENTRY_NAME = "META/zucchini_config.txt"
707
708 def ReadEntry(path, entry):
709 # Read an entry inside a .zip file or extracted dir of .zip file
710 if zipfile.is_zipfile(path):
711 with zipfile.ZipFile(path, "r", allowZip64=True) as zfp:
712 if entry in zfp.namelist():
713 return zfp.read(entry).decode()
714 else:
Zhou Xuezand0d49f52022-09-14 16:26:55 +0800715 entry_path = os.path.join(path, entry)
Kelvin Zhang410bb382022-01-06 09:15:54 -0800716 if os.path.exists(entry_path):
717 with open(entry_path, "r") as fp:
718 return fp.read()
HÃ¥kan Kvist3db1ef62022-05-03 10:19:41 +0200719 return False
720 sourceEntry = ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
721 targetEntry = ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
722 return sourceEntry and targetEntry and sourceEntry == targetEntry
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000723
724
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700725def ExtractTargetFiles(path: str):
726 if os.path.isdir(path):
727 logger.info("target files %s is already extracted", path)
728 return path
729 extracted_dir = common.MakeTempDir("target_files")
730 common.UnzipToDir(path, extracted_dir, UNZIP_PATTERN)
731 return extracted_dir
732
733
734def LocatePartitionPath(target_files_dir: str, partition: str, allow_empty):
735 path = os.path.join(target_files_dir, "RADIO", partition + ".img")
736 if os.path.exists(path):
737 return path
738 path = os.path.join(target_files_dir, "IMAGES", partition + ".img")
739 if os.path.exists(path):
740 return path
741 if allow_empty:
742 return ""
743 raise common.ExternalError(
744 "Partition {} not found in target files {}".format(partition, target_files_dir))
745
746
747def GetPartitionImages(target_files_dir: str, ab_partitions, allow_empty=True):
748 assert os.path.isdir(target_files_dir)
749 return ":".join([LocatePartitionPath(target_files_dir, partition, allow_empty) for partition in ab_partitions])
750
751
752def LocatePartitionMap(target_files_dir: str, partition: str):
753 path = os.path.join(target_files_dir, "RADIO", partition + ".map")
754 if os.path.exists(path):
755 return path
756 return ""
757
758
759def GetPartitionMaps(target_files_dir: str, ab_partitions):
760 assert os.path.isdir(target_files_dir)
761 return ":".join([LocatePartitionMap(target_files_dir, partition) for partition in ab_partitions])
762
763
Kelvin Zhangfa928692022-08-16 17:01:53 +0000764class PayloadGenerator(object):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000765 """Manages the creation and the signing of an A/B OTA Payload."""
766
767 PAYLOAD_BIN = 'payload.bin'
768 PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
769 SECONDARY_PAYLOAD_BIN = 'secondary/payload.bin'
770 SECONDARY_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
771
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700772 def __init__(self, secondary=False, wipe_user_data=False, minor_version=None, is_partial_update=False):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000773 """Initializes a Payload instance.
774
775 Args:
776 secondary: Whether it's generating a secondary payload (default: False).
777 """
778 self.payload_file = None
779 self.payload_properties = None
780 self.secondary = secondary
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000781 self.wipe_user_data = wipe_user_data
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700782 self.minor_version = minor_version
783 self.is_partial_update = is_partial_update
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000784
785 def _Run(self, cmd): # pylint: disable=no-self-use
786 # Don't pipe (buffer) the output if verbose is set. Let
787 # brillo_update_payload write to stdout/stderr directly, so its progress can
788 # be monitored.
789 if OPTIONS.verbose:
790 common.RunAndCheckOutput(cmd, stdout=None, stderr=None)
791 else:
792 common.RunAndCheckOutput(cmd)
793
794 def Generate(self, target_file, source_file=None, additional_args=None):
795 """Generates a payload from the given target-files zip(s).
796
797 Args:
798 target_file: The filename of the target build target-files zip.
799 source_file: The filename of the source build target-files zip; or None if
800 generating a full OTA.
801 additional_args: A list of additional args that should be passed to
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700802 delta_generator binary; or None.
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000803 """
804 if additional_args is None:
805 additional_args = []
806
807 payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin")
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700808 target_dir = ExtractTargetFiles(target_file)
809 cmd = ["delta_generator",
810 "--out_file", payload_file]
811 with open(os.path.join(target_dir, "META", "ab_partitions.txt")) as fp:
812 ab_partitions = fp.read().strip().split("\n")
813 cmd.extend(["--partition_names", ":".join(ab_partitions)])
814 cmd.extend(
815 ["--new_partitions", GetPartitionImages(target_dir, ab_partitions, False)])
816 cmd.extend(
817 ["--new_mapfiles", GetPartitionMaps(target_dir, ab_partitions)])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000818 if source_file is not None:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700819 source_dir = ExtractTargetFiles(source_file)
820 cmd.extend(
821 ["--old_partitions", GetPartitionImages(source_dir, ab_partitions, True)])
822 cmd.extend(
823 ["--old_mapfiles", GetPartitionMaps(source_dir, ab_partitions)])
824
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000825 if OPTIONS.disable_fec_computation:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700826 cmd.extend(["--disable_fec_computation=true"])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000827 if OPTIONS.disable_verity_computation:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700828 cmd.extend(["--disable_verity_computation=true"])
829 postinstall_config = os.path.join(
830 target_dir, "META", "postinstall_config.txt")
831
832 if os.path.exists(postinstall_config):
833 cmd.extend(["--new_postinstall_config_file", postinstall_config])
834 dynamic_partition_info = os.path.join(
835 target_dir, "META", "dynamic_partitions_info.txt")
836
837 if os.path.exists(dynamic_partition_info):
838 cmd.extend(["--dynamic_partition_info_file", dynamic_partition_info])
839
840 major_version, minor_version = ParseUpdateEngineConfig(
841 os.path.join(target_dir, "META", "update_engine_config.txt"))
842 if self.minor_version:
843 minor_version = self.minor_version
844 cmd.extend(["--major_version", str(major_version)])
845 if source_file is not None or self.is_partial_update:
846 cmd.extend(["--minor_version", str(minor_version)])
847 if self.is_partial_update:
848 cmd.extend(["--is_partial_update=true"])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000849 cmd.extend(additional_args)
850 self._Run(cmd)
851
852 self.payload_file = payload_file
853 self.payload_properties = None
854
855 def Sign(self, payload_signer):
856 """Generates and signs the hashes of the payload and metadata.
857
858 Args:
859 payload_signer: A PayloadSigner() instance that serves the signing work.
860
861 Raises:
862 AssertionError: On any failure when calling brillo_update_payload script.
863 """
864 assert isinstance(payload_signer, PayloadSigner)
865
866 # 1. Generate hashes of the payload and metadata files.
867 payload_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
868 metadata_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
869 cmd = ["brillo_update_payload", "hash",
870 "--unsigned_payload", self.payload_file,
871 "--signature_size", str(payload_signer.maximum_signature_size),
872 "--metadata_hash_file", metadata_sig_file,
873 "--payload_hash_file", payload_sig_file]
874 self._Run(cmd)
875
876 # 2. Sign the hashes.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000877 signed_payload_sig_file = payload_signer.SignHashFile(payload_sig_file)
878 signed_metadata_sig_file = payload_signer.SignHashFile(metadata_sig_file)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000879
880 # 3. Insert the signatures back into the payload file.
881 signed_payload_file = common.MakeTempFile(prefix="signed-payload-",
882 suffix=".bin")
883 cmd = ["brillo_update_payload", "sign",
884 "--unsigned_payload", self.payload_file,
885 "--payload", signed_payload_file,
886 "--signature_size", str(payload_signer.maximum_signature_size),
887 "--metadata_signature_file", signed_metadata_sig_file,
888 "--payload_signature_file", signed_payload_sig_file]
889 self._Run(cmd)
890
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000891 self.payload_file = signed_payload_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000892
893 def WriteToZip(self, output_zip):
894 """Writes the payload to the given zip.
895
896 Args:
897 output_zip: The output ZipFile instance.
898 """
899 assert self.payload_file is not None
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000900 # 4. Dump the signed payload properties.
901 properties_file = common.MakeTempFile(prefix="payload-properties-",
902 suffix=".txt")
903 cmd = ["brillo_update_payload", "properties",
904 "--payload", self.payload_file,
905 "--properties_file", properties_file]
906 self._Run(cmd)
907
908 if self.secondary:
909 with open(properties_file, "a") as f:
910 f.write("SWITCH_SLOT_ON_REBOOT=0\n")
911
912 if self.wipe_user_data:
913 with open(properties_file, "a") as f:
914 f.write("POWERWASH=1\n")
915
916 self.payload_properties = properties_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000917
918 if self.secondary:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000919 payload_arcname = PayloadGenerator.SECONDARY_PAYLOAD_BIN
920 payload_properties_arcname = PayloadGenerator.SECONDARY_PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000921 else:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000922 payload_arcname = PayloadGenerator.PAYLOAD_BIN
923 payload_properties_arcname = PayloadGenerator.PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000924
925 # Add the signed payload file and properties into the zip. In order to
926 # support streaming, we pack them as ZIP_STORED. So these entries can be
927 # read directly with the offset and length pairs.
928 common.ZipWrite(output_zip, self.payload_file, arcname=payload_arcname,
929 compress_type=zipfile.ZIP_STORED)
930 common.ZipWrite(output_zip, self.payload_properties,
931 arcname=payload_properties_arcname,
932 compress_type=zipfile.ZIP_STORED)
933
934
935class StreamingPropertyFiles(PropertyFiles):
936 """A subclass for computing the property-files for streaming A/B OTAs."""
937
938 def __init__(self):
939 super(StreamingPropertyFiles, self).__init__()
940 self.name = 'ota-streaming-property-files'
941 self.required = (
942 # payload.bin and payload_properties.txt must exist.
943 'payload.bin',
944 'payload_properties.txt',
945 )
946 self.optional = (
947 # apex_info.pb isn't directly used in the update flow
948 'apex_info.pb',
949 # care_map is available only if dm-verity is enabled.
950 'care_map.pb',
951 'care_map.txt',
952 # compatibility.zip is available only if target supports Treble.
953 'compatibility.zip',
954 )
955
956
957class AbOtaPropertyFiles(StreamingPropertyFiles):
958 """The property-files for A/B OTA that includes payload_metadata.bin info.
959
960 Since P, we expose one more token (aka property-file), in addition to the ones
961 for streaming A/B OTA, for a virtual entry of 'payload_metadata.bin'.
962 'payload_metadata.bin' is the header part of a payload ('payload.bin'), which
963 doesn't exist as a separate ZIP entry, but can be used to verify if the
964 payload can be applied on the given device.
965
966 For backward compatibility, we keep both of the 'ota-streaming-property-files'
967 and the newly added 'ota-property-files' in P. The new token will only be
968 available in 'ota-property-files'.
969 """
970
971 def __init__(self):
972 super(AbOtaPropertyFiles, self).__init__()
973 self.name = 'ota-property-files'
974
975 def _GetPrecomputed(self, input_zip):
976 offset, size = self._GetPayloadMetadataOffsetAndSize(input_zip)
977 return ['payload_metadata.bin:{}:{}'.format(offset, size)]
978
979 @staticmethod
980 def _GetPayloadMetadataOffsetAndSize(input_zip):
981 """Computes the offset and size of the payload metadata for a given package.
982
983 (From system/update_engine/update_metadata.proto)
984 A delta update file contains all the deltas needed to update a system from
985 one specific version to another specific version. The update format is
986 represented by this struct pseudocode:
987
988 struct delta_update_file {
989 char magic[4] = "CrAU";
990 uint64 file_format_version;
991 uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
992
993 // Only present if format_version > 1:
994 uint32 metadata_signature_size;
995
996 // The Bzip2 compressed DeltaArchiveManifest
997 char manifest[metadata_signature_size];
998
999 // The signature of the metadata (from the beginning of the payload up to
1000 // this location, not including the signature itself). This is a
1001 // serialized Signatures message.
1002 char medatada_signature_message[metadata_signature_size];
1003
1004 // Data blobs for files, no specific format. The specific offset
1005 // and length of each data blob is recorded in the DeltaArchiveManifest.
1006 struct {
1007 char data[];
1008 } blobs[];
1009
1010 // These two are not signed:
1011 uint64 payload_signatures_message_size;
1012 char payload_signatures_message[];
1013 };
1014
1015 'payload-metadata.bin' contains all the bytes from the beginning of the
1016 payload, till the end of 'medatada_signature_message'.
1017 """
1018 payload_info = input_zip.getinfo('payload.bin')
1019 (payload_offset, payload_size) = GetZipEntryOffset(input_zip, payload_info)
1020
1021 # Read the underlying raw zipfile at specified offset
1022 payload_fp = input_zip.fp
1023 payload_fp.seek(payload_offset)
1024 header_bin = payload_fp.read(24)
1025
1026 # network byte order (big-endian)
1027 header = struct.unpack("!IQQL", header_bin)
1028
1029 # 'CrAU'
1030 magic = header[0]
1031 assert magic == 0x43724155, "Invalid magic: {:x}, computed offset {}" \
1032 .format(magic, payload_offset)
1033
1034 manifest_size = header[2]
1035 metadata_signature_size = header[3]
1036 metadata_total = 24 + manifest_size + metadata_signature_size
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +00001037 assert metadata_total <= payload_size
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +00001038
1039 return (payload_offset, metadata_total)