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