blob: e2ce31d590babf029523f1cf44fc863467b4b46b [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,
28 GetRamdiskFormat)
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 Zhangf80e8862023-01-20 10:18:11 -0800138 logger.info(f"Signing the output file {output_file} with key {package_key}")
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000139 SignOutput(prelim_signing, output_file, package_key, pw)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400140
141 # Reopen the final signed zip to double check the streaming metadata.
Kelvin Zhang928c2342020-09-22 16:15:57 -0400142 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400143 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700144 property_files.Verify(
145 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400146
147 # If requested, dump the metadata to a separate file.
148 output_metadata_path = OPTIONS.output_metadata_path
149 if output_metadata_path:
150 WriteMetadata(metadata, output_metadata_path)
151
152
Tianjiea2076132020-08-19 17:25:32 -0700153def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400154 """Writes the metadata to the zip archive or a file.
155
156 Args:
Tianjiea2076132020-08-19 17:25:32 -0700157 metadata_proto: The metadata protobuf for the package.
158 output: A ZipFile object or a string of the output file path. If a string
159 path is given, the metadata in the protobuf format will be written to
160 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400161 """
162
Tianjiea2076132020-08-19 17:25:32 -0700163 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
164 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
165 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400166 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700167 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
168 compress_type=zipfile.ZIP_STORED)
169 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400170 compress_type=zipfile.ZIP_STORED)
171 return
172
Cole Faustb820bcd2021-10-28 13:59:48 -0700173 with open('{}.pb'.format(output), 'wb') as f:
Tianjiea2076132020-08-19 17:25:32 -0700174 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400175 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700176 f.write(legacy_metadata)
177
178
179def UpdateDeviceState(device_state, build_info, boot_variable_values,
180 is_post_build):
181 """Update the fields of the DeviceState proto with build info."""
182
Tianjie2bb14862020-08-28 16:24:34 -0700183 def UpdatePartitionStates(partition_states):
184 """Update the per-partition state according to its build.prop"""
Kelvin Zhang39aea442020-08-17 11:04:25 -0400185 if not build_info.is_ab:
186 return
Tianjie2bb14862020-08-28 16:24:34 -0700187 build_info_set = ComputeRuntimeBuildInfos(build_info,
188 boot_variable_values)
Kelvin Zhang39aea442020-08-17 11:04:25 -0400189 assert "ab_partitions" in build_info.info_dict,\
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500190 "ab_partitions property required for ab update."
Kelvin Zhang39aea442020-08-17 11:04:25 -0400191 ab_partitions = set(build_info.info_dict.get("ab_partitions"))
192
193 # delta_generator will error out on unused timestamps,
194 # so only generate timestamps for dynamic partitions
195 # used in OTA update.
Yifan Hong5057b952021-01-07 14:09:57 -0800196 for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions):
Tianjie2bb14862020-08-28 16:24:34 -0700197 partition_prop = build_info.info_dict.get(
198 '{}.build.prop'.format(partition))
199 # Skip if the partition is missing, or it doesn't have a build.prop
200 if not partition_prop or not partition_prop.build_props:
201 continue
202
203 partition_state = partition_states.add()
204 partition_state.partition_name = partition
205 # Update the partition's runtime device names and fingerprints
206 partition_devices = set()
207 partition_fingerprints = set()
208 for runtime_build_info in build_info_set:
209 partition_devices.add(
210 runtime_build_info.GetPartitionBuildProp('ro.product.device',
211 partition))
212 partition_fingerprints.add(
213 runtime_build_info.GetPartitionFingerprint(partition))
214
215 partition_state.device.extend(sorted(partition_devices))
216 partition_state.build.extend(sorted(partition_fingerprints))
217
218 # TODO(xunchang) set the boot image's version with kmi. Note the boot
219 # image doesn't have a file map.
220 partition_state.version = build_info.GetPartitionBuildProp(
221 'ro.build.date.utc', partition)
222
223 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos.
Tianjiea2076132020-08-19 17:25:32 -0700224 build_devices, build_fingerprints = \
225 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
226 device_state.device.extend(sorted(build_devices))
227 device_state.build.extend(sorted(build_fingerprints))
228 device_state.build_incremental = build_info.GetBuildProp(
229 'ro.build.version.incremental')
230
Tianjie2bb14862020-08-28 16:24:34 -0700231 UpdatePartitionStates(device_state.partition_state)
Tianjiea2076132020-08-19 17:25:32 -0700232
233 if is_post_build:
234 device_state.sdk_level = build_info.GetBuildProp(
235 'ro.build.version.sdk')
236 device_state.security_patch_level = build_info.GetBuildProp(
237 'ro.build.version.security_patch')
238 # Use the actual post-timestamp, even for a downgrade case.
239 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400240
241
242def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700243 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400244
Tianjiea2076132020-08-19 17:25:32 -0700245 It generates a ota_metadata protobuf that contains the info to be written
246 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
247 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400248
249 Args:
250 target_info: The BuildInfo instance that holds the target build info.
251 source_info: The BuildInfo instance that holds the source build info, or
252 None if generating full OTA.
253
254 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700255 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400256 """
257 assert isinstance(target_info, BuildInfo)
258 assert source_info is None or isinstance(source_info, BuildInfo)
259
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400260 boot_variable_values = {}
261 if OPTIONS.boot_variable_file:
262 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
263 for key, values in d.items():
264 boot_variable_values[key] = [val.strip() for val in values.split(',')]
265
Tianjiea2076132020-08-19 17:25:32 -0700266 metadata_proto = ota_metadata_pb2.OtaMetadata()
267 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
268 # consider skipping them if they aren't used by clients.
269 UpdateDeviceState(metadata_proto.postcondition, target_info,
270 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400271
272 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700273 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
274 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400275 else:
Tianjiea2076132020-08-19 17:25:32 -0700276 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
277 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400278
279 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700280 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400281
282 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700283 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400284
285 is_incremental = source_info is not None
286 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700287 UpdateDeviceState(metadata_proto.precondition, source_info,
288 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400289 else:
Tianjiea2076132020-08-19 17:25:32 -0700290 metadata_proto.precondition.device.extend(
291 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400292
293 # Detect downgrades and set up downgrade flags accordingly.
294 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700295 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400296
Tianjiea2076132020-08-19 17:25:32 -0700297 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400298
299
Tianjiea2076132020-08-19 17:25:32 -0700300def BuildLegacyOtaMetadata(metadata_proto):
301 """Converts the metadata proto to a legacy metadata dict.
302
303 This metadata dict is used to build the legacy metadata text file for
304 backward compatibility. We won't add new keys to the legacy metadata format.
305 If new information is needed, we should add it as a new field in OtaMetadata
306 proto definition.
307 """
308
309 separator = '|'
310
311 metadata_dict = {}
312 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
313 metadata_dict['ota-type'] = 'AB'
314 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
315 metadata_dict['ota-type'] = 'BLOCK'
316 if metadata_proto.wipe:
317 metadata_dict['ota-wipe'] = 'yes'
318 if metadata_proto.retrofit_dynamic_partitions:
319 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
320 if metadata_proto.downgrade:
321 metadata_dict['ota-downgrade'] = 'yes'
322
323 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
324
325 post_build = metadata_proto.postcondition
326 metadata_dict['post-build'] = separator.join(post_build.build)
327 metadata_dict['post-build-incremental'] = post_build.build_incremental
328 metadata_dict['post-sdk-level'] = post_build.sdk_level
329 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
330 metadata_dict['post-timestamp'] = str(post_build.timestamp)
331
332 pre_build = metadata_proto.precondition
333 metadata_dict['pre-device'] = separator.join(pre_build.device)
334 # incremental updates
335 if len(pre_build.build) != 0:
336 metadata_dict['pre-build'] = separator.join(pre_build.build)
337 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
338
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500339 if metadata_proto.spl_downgrade:
340 metadata_dict['spl-downgrade'] = 'yes'
Tianjiea2076132020-08-19 17:25:32 -0700341 metadata_dict.update(metadata_proto.property_files)
342
343 return metadata_dict
344
345
346def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400347 # Only incremental OTAs are allowed to reach here.
348 assert OPTIONS.incremental_source is not None
349
350 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
351 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
352 is_downgrade = int(post_timestamp) < int(pre_timestamp)
353
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500354 if OPTIONS.spl_downgrade:
355 metadata_proto.spl_downgrade = True
356
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400357 if OPTIONS.downgrade:
358 if not is_downgrade:
359 raise RuntimeError(
360 "--downgrade or --override_timestamp specified but no downgrade "
361 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700362 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400363 else:
364 if is_downgrade:
365 raise RuntimeError(
366 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
367 "Need to specify --override_timestamp OR --downgrade to allow "
368 "building the incremental." % (pre_timestamp, post_timestamp))
369
370
Tianjie2bb14862020-08-28 16:24:34 -0700371def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
372 """Returns a set of build info objects that may exist during runtime."""
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400373
Tianjie2bb14862020-08-28 16:24:34 -0700374 build_info_set = {default_build_info}
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400375 if not boot_variable_values:
Tianjie2bb14862020-08-28 16:24:34 -0700376 return build_info_set
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400377
378 # Calculate all possible combinations of the values for the boot variables.
379 keys = boot_variable_values.keys()
380 value_list = boot_variable_values.values()
381 combinations = [dict(zip(keys, values))
382 for values in itertools.product(*value_list)]
383 for placeholder_values in combinations:
384 # Reload the info_dict as some build properties may change their values
385 # based on the value of ro.boot* properties.
Tianjie2bb14862020-08-28 16:24:34 -0700386 info_dict = copy.deepcopy(default_build_info.info_dict)
Yifan Hong5057b952021-01-07 14:09:57 -0800387 for partition in PARTITIONS_WITH_BUILD_PROP:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400388 partition_prop_key = "{}.build.prop".format(partition)
389 input_file = info_dict[partition_prop_key].input_file
TJ Rhoades6f488e92022-05-01 22:16:22 -0700390 ramdisk = GetRamdiskFormat(info_dict)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400391 if isinstance(input_file, zipfile.ZipFile):
Kelvin Zhang928c2342020-09-22 16:15:57 -0400392 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400393 info_dict[partition_prop_key] = \
394 PartitionBuildProps.FromInputFile(input_zip, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700395 placeholder_values,
396 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400397 else:
398 info_dict[partition_prop_key] = \
399 PartitionBuildProps.FromInputFile(input_file, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700400 placeholder_values,
401 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400402 info_dict["build.prop"] = info_dict["system.build.prop"]
Tianjie2bb14862020-08-28 16:24:34 -0700403 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400404
Tianjie2bb14862020-08-28 16:24:34 -0700405 return build_info_set
406
407
408def CalculateRuntimeDevicesAndFingerprints(default_build_info,
409 boot_variable_values):
410 """Returns a tuple of sets for runtime devices and fingerprints"""
411
412 device_names = set()
413 fingerprints = set()
414 build_info_set = ComputeRuntimeBuildInfos(default_build_info,
415 boot_variable_values)
416 for runtime_build_info in build_info_set:
417 device_names.add(runtime_build_info.device)
418 fingerprints.add(runtime_build_info.fingerprint)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400419 return device_names, fingerprints
420
421
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400422def GetZipEntryOffset(zfp, entry_info):
423 """Get offset to a beginning of a particular zip entry
424 Args:
425 fp: zipfile.ZipFile
426 entry_info: zipfile.ZipInfo
427
428 Returns:
429 (offset, size) tuple
430 """
431 # Don't use len(entry_info.extra). Because that returns size of extra
432 # fields in central directory. We need to look at local file directory,
433 # as these two might have different sizes.
434
435 # We cannot work with zipfile.ZipFile instances, we need a |fp| for the underlying file.
436 zfp = zfp.fp
437 zfp.seek(entry_info.header_offset)
438 data = zfp.read(zipfile.sizeFileHeader)
439 fheader = struct.unpack(zipfile.structFileHeader, data)
440 # Last two fields of local file header are filename length and
441 # extra length
442 filename_len = fheader[-2]
443 extra_len = fheader[-1]
444 offset = entry_info.header_offset
445 offset += zipfile.sizeFileHeader
446 offset += filename_len + extra_len
447 size = entry_info.file_size
448 return (offset, size)
449
450
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400451class PropertyFiles(object):
452 """A class that computes the property-files string for an OTA package.
453
454 A property-files string is a comma-separated string that contains the
455 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
456 can be fetched directly with the package URL along with the offset/size info.
457 These strings can be used for streaming A/B OTAs, or allowing an updater to
458 download package metadata entry directly, without paying the cost of
459 downloading entire package.
460
461 Computing the final property-files string requires two passes. Because doing
462 the whole package signing (with signapk.jar) will possibly reorder the ZIP
463 entries, which may in turn invalidate earlier computed ZIP entry offset/size
464 values.
465
466 This class provides functions to be called for each pass. The general flow is
467 as follows.
468
469 property_files = PropertyFiles()
470 # The first pass, which writes placeholders before doing initial signing.
471 property_files.Compute()
472 SignOutput()
473
474 # The second pass, by replacing the placeholders with actual data.
475 property_files.Finalize()
476 SignOutput()
477
478 And the caller can additionally verify the final result.
479
480 property_files.Verify()
481 """
482
483 def __init__(self):
484 self.name = None
485 self.required = ()
486 self.optional = ()
487
488 def Compute(self, input_zip):
489 """Computes and returns a property-files string with placeholders.
490
491 We reserve extra space for the offset and size of the metadata entry itself,
492 although we don't know the final values until the package gets signed.
493
494 Args:
495 input_zip: The input ZIP file.
496
497 Returns:
498 A string with placeholders for the metadata offset/size info, e.g.
499 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
500 """
501 return self.GetPropertyFilesString(input_zip, reserve_space=True)
502
503 class InsufficientSpaceException(Exception):
504 pass
505
506 def Finalize(self, input_zip, reserved_length):
507 """Finalizes a property-files string with actual METADATA offset/size info.
508
509 The input ZIP file has been signed, with the ZIP entries in the desired
510 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
511 the ZIP entry offsets and construct the property-files string with actual
512 data. Note that during this process, we must pad the property-files string
513 to the reserved length, so that the METADATA entry size remains the same.
514 Otherwise the entries' offsets and sizes may change again.
515
516 Args:
517 input_zip: The input ZIP file.
518 reserved_length: The reserved length of the property-files string during
519 the call to Compute(). The final string must be no more than this
520 size.
521
522 Returns:
523 A property-files string including the metadata offset/size info, e.g.
524 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
525
526 Raises:
527 InsufficientSpaceException: If the reserved length is insufficient to hold
528 the final string.
529 """
530 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
531 if len(result) > reserved_length:
532 raise self.InsufficientSpaceException(
533 'Insufficient reserved space: reserved={}, actual={}'.format(
534 reserved_length, len(result)))
535
536 result += ' ' * (reserved_length - len(result))
537 return result
538
539 def Verify(self, input_zip, expected):
540 """Verifies the input ZIP file contains the expected property-files string.
541
542 Args:
543 input_zip: The input ZIP file.
544 expected: The property-files string that's computed from Finalize().
545
546 Raises:
547 AssertionError: On finding a mismatch.
548 """
549 actual = self.GetPropertyFilesString(input_zip)
550 assert actual == expected, \
551 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
552
553 def GetPropertyFilesString(self, zip_file, reserve_space=False):
554 """
555 Constructs the property-files string per request.
556
557 Args:
558 zip_file: The input ZIP file.
559 reserved_length: The reserved length of the property-files string.
560
561 Returns:
562 A property-files string including the metadata offset/size info, e.g.
563 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
564 """
565
566 def ComputeEntryOffsetSize(name):
567 """Computes the zip entry offset and size."""
568 info = zip_file.getinfo(name)
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400569 (offset, size) = GetZipEntryOffset(zip_file, info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400570 return '%s:%d:%d' % (os.path.basename(name), offset, size)
571
572 tokens = []
573 tokens.extend(self._GetPrecomputed(zip_file))
574 for entry in self.required:
575 tokens.append(ComputeEntryOffsetSize(entry))
576 for entry in self.optional:
577 if entry in zip_file.namelist():
578 tokens.append(ComputeEntryOffsetSize(entry))
579
580 # 'META-INF/com/android/metadata' is required. We don't know its actual
581 # offset and length (as well as the values for other entries). So we reserve
582 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
583 # the space for metadata entry. Because 'offset' allows a max of 10-digit
584 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
585 # reserved space serves the metadata entry only.
586 if reserve_space:
587 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700588 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400589 else:
590 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Luca Stefanib6075c52021-11-03 17:10:54 +0100591 if METADATA_PROTO_NAME in zip_file.namelist():
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700592 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400593
594 return ','.join(tokens)
595
596 def _GetPrecomputed(self, input_zip):
597 """Computes the additional tokens to be included into the property-files.
598
599 This applies to tokens without actual ZIP entries, such as
600 payload_metadata.bin. We want to expose the offset/size to updaters, so
601 that they can download the payload metadata directly with the info.
602
603 Args:
604 input_zip: The input zip file.
605
606 Returns:
607 A list of strings (tokens) to be added to the property-files string.
608 """
609 # pylint: disable=no-self-use
610 # pylint: disable=unused-argument
611 return []
612
613
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000614def SignOutput(temp_zip_name, output_zip_name, package_key=None, pw=None):
615 if package_key is None:
616 package_key = OPTIONS.package_key
617 if pw is None and OPTIONS.key_passwords:
618 pw = OPTIONS.key_passwords[package_key]
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400619
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000620 SignFile(temp_zip_name, output_zip_name, package_key, pw,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400621 whole_file=True)
Tianjiea5fca032021-06-01 22:06:28 -0700622
623
624def ConstructOtaApexInfo(target_zip, source_file=None):
625 """If applicable, add the source version to the apex info."""
626
627 def _ReadApexInfo(input_zip):
628 if "META/apex_info.pb" not in input_zip.namelist():
629 logger.warning("target_file doesn't contain apex_info.pb %s", input_zip)
630 return None
631
632 with input_zip.open("META/apex_info.pb", "r") as zfp:
633 return zfp.read()
634
635 target_apex_string = _ReadApexInfo(target_zip)
636 # Return early if the target apex info doesn't exist or is empty.
637 if not target_apex_string:
638 return target_apex_string
639
640 # If the source apex info isn't available, just return the target info
641 if not source_file:
642 return target_apex_string
643
644 with zipfile.ZipFile(source_file, "r", allowZip64=True) as source_zip:
645 source_apex_string = _ReadApexInfo(source_zip)
646 if not source_apex_string:
647 return target_apex_string
648
649 source_apex_proto = ota_metadata_pb2.ApexMetadata()
650 source_apex_proto.ParseFromString(source_apex_string)
651 source_apex_versions = {apex.package_name: apex.version for apex in
652 source_apex_proto.apex_info}
653
654 # If the apex package is available in the source build, initialize the source
655 # apex version.
656 target_apex_proto = ota_metadata_pb2.ApexMetadata()
657 target_apex_proto.ParseFromString(target_apex_string)
658 for target_apex in target_apex_proto.apex_info:
659 name = target_apex.package_name
660 if name in source_apex_versions:
661 target_apex.source_version = source_apex_versions[name]
662
663 return target_apex_proto.SerializeToString()
Kelvin Zhang410bb382022-01-06 09:15:54 -0800664
665
Kelvin Zhangf2728d62022-01-10 11:42:36 -0800666def IsLz4diffCompatible(source_file: str, target_file: str):
667 """Check whether lz4diff versions in two builds are compatible
668
669 Args:
670 source_file: Path to source build's target_file.zip
671 target_file: Path to target build's target_file.zip
672
673 Returns:
674 bool true if and only if lz4diff versions are compatible
675 """
676 if source_file is None or target_file is None:
677 return False
678 # Right now we enable lz4diff as long as source build has liblz4.so.
679 # In the future we might introduce version system to lz4diff as well.
680 if zipfile.is_zipfile(source_file):
681 with zipfile.ZipFile(source_file, "r") as zfp:
682 return "META/liblz4.so" in zfp.namelist()
683 else:
684 assert os.path.isdir(source_file)
685 return os.path.exists(os.path.join(source_file, "META", "liblz4.so"))
686
687
Kelvin Zhang410bb382022-01-06 09:15:54 -0800688def IsZucchiniCompatible(source_file: str, target_file: str):
689 """Check whether zucchini versions in two builds are compatible
690
691 Args:
692 source_file: Path to source build's target_file.zip
693 target_file: Path to target build's target_file.zip
694
695 Returns:
696 bool true if and only if zucchini versions are compatible
697 """
698 if source_file is None or target_file is None:
699 return False
700 assert os.path.exists(source_file)
701 assert os.path.exists(target_file)
702
703 assert zipfile.is_zipfile(source_file) or os.path.isdir(source_file)
704 assert zipfile.is_zipfile(target_file) or os.path.isdir(target_file)
705 _ZUCCHINI_CONFIG_ENTRY_NAME = "META/zucchini_config.txt"
706
707 def ReadEntry(path, entry):
708 # Read an entry inside a .zip file or extracted dir of .zip file
709 if zipfile.is_zipfile(path):
710 with zipfile.ZipFile(path, "r", allowZip64=True) as zfp:
711 if entry in zfp.namelist():
712 return zfp.read(entry).decode()
713 else:
Zhou Xuezand0d49f52022-09-14 16:26:55 +0800714 entry_path = os.path.join(path, entry)
Kelvin Zhang410bb382022-01-06 09:15:54 -0800715 if os.path.exists(entry_path):
716 with open(entry_path, "r") as fp:
717 return fp.read()
HÃ¥kan Kvist3db1ef62022-05-03 10:19:41 +0200718 return False
719 sourceEntry = ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
720 targetEntry = ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
721 return sourceEntry and targetEntry and sourceEntry == targetEntry
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000722
723
Kelvin Zhangfa928692022-08-16 17:01:53 +0000724class PayloadGenerator(object):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000725 """Manages the creation and the signing of an A/B OTA Payload."""
726
727 PAYLOAD_BIN = 'payload.bin'
728 PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
729 SECONDARY_PAYLOAD_BIN = 'secondary/payload.bin'
730 SECONDARY_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
731
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000732 def __init__(self, secondary=False, wipe_user_data=False):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000733 """Initializes a Payload instance.
734
735 Args:
736 secondary: Whether it's generating a secondary payload (default: False).
737 """
738 self.payload_file = None
739 self.payload_properties = None
740 self.secondary = secondary
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000741 self.wipe_user_data = wipe_user_data
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000742
743 def _Run(self, cmd): # pylint: disable=no-self-use
744 # Don't pipe (buffer) the output if verbose is set. Let
745 # brillo_update_payload write to stdout/stderr directly, so its progress can
746 # be monitored.
747 if OPTIONS.verbose:
748 common.RunAndCheckOutput(cmd, stdout=None, stderr=None)
749 else:
750 common.RunAndCheckOutput(cmd)
751
752 def Generate(self, target_file, source_file=None, additional_args=None):
753 """Generates a payload from the given target-files zip(s).
754
755 Args:
756 target_file: The filename of the target build target-files zip.
757 source_file: The filename of the source build target-files zip; or None if
758 generating a full OTA.
759 additional_args: A list of additional args that should be passed to
760 brillo_update_payload script; or None.
761 """
762 if additional_args is None:
763 additional_args = []
764
765 payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin")
766 cmd = ["brillo_update_payload", "generate",
767 "--payload", payload_file,
768 "--target_image", target_file]
769 if source_file is not None:
770 cmd.extend(["--source_image", source_file])
771 if OPTIONS.disable_fec_computation:
772 cmd.extend(["--disable_fec_computation", "true"])
773 if OPTIONS.disable_verity_computation:
774 cmd.extend(["--disable_verity_computation", "true"])
775 cmd.extend(additional_args)
776 self._Run(cmd)
777
778 self.payload_file = payload_file
779 self.payload_properties = None
780
781 def Sign(self, payload_signer):
782 """Generates and signs the hashes of the payload and metadata.
783
784 Args:
785 payload_signer: A PayloadSigner() instance that serves the signing work.
786
787 Raises:
788 AssertionError: On any failure when calling brillo_update_payload script.
789 """
790 assert isinstance(payload_signer, PayloadSigner)
791
792 # 1. Generate hashes of the payload and metadata files.
793 payload_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
794 metadata_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
795 cmd = ["brillo_update_payload", "hash",
796 "--unsigned_payload", self.payload_file,
797 "--signature_size", str(payload_signer.maximum_signature_size),
798 "--metadata_hash_file", metadata_sig_file,
799 "--payload_hash_file", payload_sig_file]
800 self._Run(cmd)
801
802 # 2. Sign the hashes.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000803 signed_payload_sig_file = payload_signer.SignHashFile(payload_sig_file)
804 signed_metadata_sig_file = payload_signer.SignHashFile(metadata_sig_file)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000805
806 # 3. Insert the signatures back into the payload file.
807 signed_payload_file = common.MakeTempFile(prefix="signed-payload-",
808 suffix=".bin")
809 cmd = ["brillo_update_payload", "sign",
810 "--unsigned_payload", self.payload_file,
811 "--payload", signed_payload_file,
812 "--signature_size", str(payload_signer.maximum_signature_size),
813 "--metadata_signature_file", signed_metadata_sig_file,
814 "--payload_signature_file", signed_payload_sig_file]
815 self._Run(cmd)
816
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000817 self.payload_file = signed_payload_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000818
819 def WriteToZip(self, output_zip):
820 """Writes the payload to the given zip.
821
822 Args:
823 output_zip: The output ZipFile instance.
824 """
825 assert self.payload_file is not None
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000826 # 4. Dump the signed payload properties.
827 properties_file = common.MakeTempFile(prefix="payload-properties-",
828 suffix=".txt")
829 cmd = ["brillo_update_payload", "properties",
830 "--payload", self.payload_file,
831 "--properties_file", properties_file]
832 self._Run(cmd)
833
834 if self.secondary:
835 with open(properties_file, "a") as f:
836 f.write("SWITCH_SLOT_ON_REBOOT=0\n")
837
838 if self.wipe_user_data:
839 with open(properties_file, "a") as f:
840 f.write("POWERWASH=1\n")
841
842 self.payload_properties = properties_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000843
844 if self.secondary:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000845 payload_arcname = PayloadGenerator.SECONDARY_PAYLOAD_BIN
846 payload_properties_arcname = PayloadGenerator.SECONDARY_PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000847 else:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000848 payload_arcname = PayloadGenerator.PAYLOAD_BIN
849 payload_properties_arcname = PayloadGenerator.PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000850
851 # Add the signed payload file and properties into the zip. In order to
852 # support streaming, we pack them as ZIP_STORED. So these entries can be
853 # read directly with the offset and length pairs.
854 common.ZipWrite(output_zip, self.payload_file, arcname=payload_arcname,
855 compress_type=zipfile.ZIP_STORED)
856 common.ZipWrite(output_zip, self.payload_properties,
857 arcname=payload_properties_arcname,
858 compress_type=zipfile.ZIP_STORED)
859
860
861class StreamingPropertyFiles(PropertyFiles):
862 """A subclass for computing the property-files for streaming A/B OTAs."""
863
864 def __init__(self):
865 super(StreamingPropertyFiles, self).__init__()
866 self.name = 'ota-streaming-property-files'
867 self.required = (
868 # payload.bin and payload_properties.txt must exist.
869 'payload.bin',
870 'payload_properties.txt',
871 )
872 self.optional = (
873 # apex_info.pb isn't directly used in the update flow
874 'apex_info.pb',
875 # care_map is available only if dm-verity is enabled.
876 'care_map.pb',
877 'care_map.txt',
878 # compatibility.zip is available only if target supports Treble.
879 'compatibility.zip',
880 )
881
882
883class AbOtaPropertyFiles(StreamingPropertyFiles):
884 """The property-files for A/B OTA that includes payload_metadata.bin info.
885
886 Since P, we expose one more token (aka property-file), in addition to the ones
887 for streaming A/B OTA, for a virtual entry of 'payload_metadata.bin'.
888 'payload_metadata.bin' is the header part of a payload ('payload.bin'), which
889 doesn't exist as a separate ZIP entry, but can be used to verify if the
890 payload can be applied on the given device.
891
892 For backward compatibility, we keep both of the 'ota-streaming-property-files'
893 and the newly added 'ota-property-files' in P. The new token will only be
894 available in 'ota-property-files'.
895 """
896
897 def __init__(self):
898 super(AbOtaPropertyFiles, self).__init__()
899 self.name = 'ota-property-files'
900
901 def _GetPrecomputed(self, input_zip):
902 offset, size = self._GetPayloadMetadataOffsetAndSize(input_zip)
903 return ['payload_metadata.bin:{}:{}'.format(offset, size)]
904
905 @staticmethod
906 def _GetPayloadMetadataOffsetAndSize(input_zip):
907 """Computes the offset and size of the payload metadata for a given package.
908
909 (From system/update_engine/update_metadata.proto)
910 A delta update file contains all the deltas needed to update a system from
911 one specific version to another specific version. The update format is
912 represented by this struct pseudocode:
913
914 struct delta_update_file {
915 char magic[4] = "CrAU";
916 uint64 file_format_version;
917 uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
918
919 // Only present if format_version > 1:
920 uint32 metadata_signature_size;
921
922 // The Bzip2 compressed DeltaArchiveManifest
923 char manifest[metadata_signature_size];
924
925 // The signature of the metadata (from the beginning of the payload up to
926 // this location, not including the signature itself). This is a
927 // serialized Signatures message.
928 char medatada_signature_message[metadata_signature_size];
929
930 // Data blobs for files, no specific format. The specific offset
931 // and length of each data blob is recorded in the DeltaArchiveManifest.
932 struct {
933 char data[];
934 } blobs[];
935
936 // These two are not signed:
937 uint64 payload_signatures_message_size;
938 char payload_signatures_message[];
939 };
940
941 'payload-metadata.bin' contains all the bytes from the beginning of the
942 payload, till the end of 'medatada_signature_message'.
943 """
944 payload_info = input_zip.getinfo('payload.bin')
945 (payload_offset, payload_size) = GetZipEntryOffset(input_zip, payload_info)
946
947 # Read the underlying raw zipfile at specified offset
948 payload_fp = input_zip.fp
949 payload_fp.seek(payload_offset)
950 header_bin = payload_fp.read(24)
951
952 # network byte order (big-endian)
953 header = struct.unpack("!IQQL", header_bin)
954
955 # 'CrAU'
956 magic = header[0]
957 assert magic == 0x43724155, "Invalid magic: {:x}, computed offset {}" \
958 .format(magic, payload_offset)
959
960 manifest_size = header[2]
961 metadata_signature_size = header[3]
962 metadata_total = 24 + manifest_size + metadata_signature_size
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000963 assert metadata_total <= payload_size
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000964
965 return (payload_offset, metadata_total)