1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3 4# Copyright (c) 2021 Huawei Device Co., Ltd. 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16import bisect 17import copy 18import os 19import struct 20import tempfile 21from hashlib import sha256 22 23from log_exception import UPDATE_LOGGER 24from blocks_manager import BlocksManager 25from utils import SPARSE_IMAGE_MAGIC 26from utils import HEADER_INFO_FORMAT 27from utils import HEADER_INFO_LEN 28from utils import EXTEND_VALUE 29from utils import FILE_MAP_ZERO_KEY 30from utils import FILE_MAP_NONZERO_KEY 31from utils import FILE_MAP_COPY_KEY 32from utils import MAX_BLOCKS_PER_GROUP 33 34 35class FullUpdateImage: 36 """ 37 Full image processing class 38 """ 39 40 def __init__(self, target_package_images_dir, full_img_list, verse_script, 41 full_image_path_list, no_zip=False): 42 self.__target_package_images_dir = target_package_images_dir 43 self.__full_img_list = full_img_list 44 self.__verse_script = verse_script 45 self.__full_image_path_list = full_image_path_list 46 self.__no_zip = no_zip 47 48 def update_full_image(self): 49 """ 50 Processing of the full image 51 :return full_image_content_len_list: full image content length list 52 :return full_image_file_obj_list: full image temporary file list 53 """ 54 full_image_file_obj_list = [] 55 full_image_content_len_list = [] 56 for idx, each_name in enumerate(self.__full_img_list): 57 full_image_content = self.get_full_image_content( 58 self.__full_image_path_list[idx]) 59 if full_image_content is False: 60 UPDATE_LOGGER.print_log( 61 "Get full image content failed!", 62 log_type=UPDATE_LOGGER.ERROR_LOG) 63 return False, False 64 each_img = tempfile.NamedTemporaryFile( 65 prefix="full_image%s" % each_name, mode='wb') 66 each_img.write(full_image_content) 67 each_img.seek(0) 68 full_image_content_len_list.append(len(full_image_content)) 69 full_image_file_obj_list.append(each_img) 70 UPDATE_LOGGER.print_log( 71 "Image %s full processing completed" % each_name) 72 if not self.__no_zip: 73 # No zip mode (no script command) 74 if is_sparse_image(each_img.name): 75 sparse_image_write_cmd = \ 76 self.__verse_script.sparse_image_write(each_name) 77 cmd = '%s_WRITE_FLAG%s' % ( 78 each_name, sparse_image_write_cmd) 79 else: 80 raw_image_write_cmd = \ 81 self.__verse_script.raw_image_write( 82 each_name, each_name) 83 cmd = '%s_WRITE_FLAG%s' % ( 84 each_name, raw_image_write_cmd) 85 if each_name not in ("boot", "updater_boot", 86 "updater", "updater_b"): 87 self.__verse_script.add_command( 88 cmd=cmd) 89 90 UPDATE_LOGGER.print_log( 91 "All full image processing completed! image count: %d" % 92 len(self.__full_img_list)) 93 return full_image_content_len_list, full_image_file_obj_list 94 95 @staticmethod 96 def get_full_image_content(each_name): 97 """ 98 Obtain the full image content. 99 :param each_name: image name 100 :return content: full image content if available; false otherwise 101 """ 102 each_image_path = each_name 103 if not os.path.exists(each_image_path): 104 UPDATE_LOGGER.print_log( 105 "The file is missing " 106 "from the target package, " 107 "the component: %s cannot be full update processed. " % 108 each_image_path) 109 return False 110 with open(each_image_path, 'rb') as f_r: 111 content = f_r.read() 112 return content 113 114 115def is_sparse_image(img_path): 116 """ 117 Check whether the image is a sparse image. 118 :param img_path: image path 119 :return: 120 """ 121 with open(img_path, 'rb') as f_r: 122 image_content = f_r.read(HEADER_INFO_LEN) 123 try: 124 header_info = struct.unpack(HEADER_INFO_FORMAT, image_content) 125 except struct.error: 126 return False 127 is_sparse = IncUpdateImage.image_header_info_check(header_info)[-1] 128 if is_sparse: 129 UPDATE_LOGGER.print_log("Sparse image is not supported!") 130 raise RuntimeError 131 return is_sparse 132 133 134class IncUpdateImage: 135 """ 136 Increment update image class 137 """ 138 139 def __init__(self, image_path, map_path): 140 """ 141 Initialize the inc image. 142 :param image_path: img file path 143 :param map_path: map file path 144 """ 145 self.image_path = image_path 146 self.offset_value_list = [] 147 self.care_block_range = None 148 self.extended_range = None 149 self.reserved_blocks = BlocksManager("0") 150 self.file_map = [] 151 self.offset_index = [] 152 self.block_size = None 153 self.total_blocks = None 154 self.parse_sparse_image_file(image_path, map_path) 155 156 def parse_sparse_image_file(self, image_path, map_path): 157 """ 158 Parse the .img file. 159 :param image_path: img file path 160 :param map_path: map file path 161 """ 162 self.block_size = block_size = 4096 163 self.total_blocks = total_blocks = \ 164 os.path.getsize(self.image_path) // self.block_size 165 reference = b'\0' * self.block_size 166 with open(image_path, 'rb') as f_r: 167 care_value_list, offset_value_list = [], [] 168 nonzero_blocks = [] 169 for i in range(self.total_blocks): 170 blocks_data = f_r.read(self.block_size) 171 if blocks_data != reference: 172 nonzero_blocks.append(i) 173 nonzero_blocks.append(i + 1) 174 self.care_block_range = BlocksManager(nonzero_blocks) 175 care_value_list = list(self.care_block_range.range_data) 176 for idx, value in enumerate(care_value_list): 177 if idx != 0 and (idx + 1) % 2 == 0: 178 be_value = int(care_value_list[idx - 1]) 179 af_value = int(care_value_list[idx]) 180 file_tell = be_value * block_size 181 offset_value_list.append( 182 (be_value, af_value - be_value, 183 file_tell, None)) 184 185 self.offset_index = [i[0] for i in offset_value_list] 186 self.offset_value_list = offset_value_list 187 extended_range = \ 188 self.care_block_range.extend_value_to_blocks(EXTEND_VALUE) 189 all_blocks = BlocksManager(range_data=(0, total_blocks)) 190 self.extended_range = \ 191 extended_range.get_intersect_with_other(all_blocks). \ 192 get_subtract_with_other(self.care_block_range) 193 self.parse_block_map_file(map_path, f_r) 194 195 def parse_block_map_file(self, map_path, image_file_r): 196 """ 197 Parses the map file for blocks where files are contained in the image. 198 :param map_path: map file path 199 :param image_file_r: file reading object 200 :return: 201 """ 202 remain_range = self.care_block_range 203 temp_file_map = {} 204 205 with open(map_path, 'r') as f_r: 206 # Read the .map file and process each line. 207 for each_line in f_r.readlines(): 208 each_map_path, ranges_value = each_line.split(None, 1) 209 each_range = BlocksManager(ranges_value) 210 temp_file_map[each_map_path] = each_range 211 # each_range is contained in the remain range. 212 if each_range.size() != each_range. \ 213 get_intersect_with_other(remain_range).size(): 214 raise RuntimeError 215 # After the processing is complete, 216 # remove each_range from remain_range. 217 remain_range = remain_range.get_subtract_with_other(each_range) 218 reserved_blocks = self.reserved_blocks 219 # Remove reserved blocks from all blocks. 220 remain_range = remain_range.get_subtract_with_other(reserved_blocks) 221 222 # Divide all blocks into zero_blocks 223 # (if there are many) and nonzero_blocks. 224 zero_blocks_list = [] 225 nonzero_blocks_list = [] 226 nonzero_groups_list = [] 227 default_zero_block = ('\0' * self.block_size).encode() 228 229 nonzero_blocks_list, nonzero_groups_list, zero_blocks_list = \ 230 self.apply_remain_range( 231 default_zero_block, image_file_r, nonzero_blocks_list, 232 nonzero_groups_list, remain_range, zero_blocks_list) 233 234 temp_file_map = self.get_file_map( 235 nonzero_blocks_list, nonzero_groups_list, 236 reserved_blocks, temp_file_map, zero_blocks_list) 237 self.file_map = temp_file_map 238 239 def apply_remain_range(self, *args): 240 """ 241 Implement traversal processing of remain_range. 242 """ 243 default_zero_block, image_file_r, \ 244 nonzero_blocks_list, nonzero_groups_list, \ 245 remain_range, zero_blocks_list = args 246 for start_value, end_value in remain_range: 247 for each_value in range(start_value, end_value): 248 # bisect 二分查找,b在self.offset_index中的位置 249 idx = bisect.bisect_right(self.offset_index, each_value) - 1 250 chunk_start, _, file_pos, fill_data = \ 251 self.offset_value_list[idx] 252 data = self.get_file_data(self.block_size, chunk_start, 253 default_zero_block, each_value, 254 file_pos, fill_data, image_file_r) 255 256 zero_blocks_list, nonzero_blocks_list, nonzero_groups_list = \ 257 self.get_zero_nonzero_blocks_list( 258 data, default_zero_block, each_value, 259 nonzero_blocks_list, nonzero_groups_list, 260 zero_blocks_list) 261 return nonzero_blocks_list, nonzero_groups_list, zero_blocks_list 262 263 @staticmethod 264 def get_file_map(*args): 265 """ 266 Obtain the file map. 267 nonzero_blocks_list nonzero blocks list, 268 nonzero_groups_list nonzero groups list, 269 reserved_blocks reserved blocks , 270 temp_file_map temporary file map, 271 zero_blocks_list zero block list 272 :return temp_file_map file map 273 """ 274 nonzero_blocks_list, nonzero_groups_list, \ 275 reserved_blocks, temp_file_map, zero_blocks_list = args 276 if nonzero_blocks_list: 277 nonzero_groups_list.append(nonzero_blocks_list) 278 if zero_blocks_list: 279 temp_file_map[FILE_MAP_ZERO_KEY] = \ 280 BlocksManager(range_data=zero_blocks_list) 281 if nonzero_groups_list: 282 for i, blocks in enumerate(nonzero_groups_list): 283 temp_file_map["%s-%d" % (FILE_MAP_NONZERO_KEY, i)] = \ 284 BlocksManager(range_data=blocks) 285 if reserved_blocks: 286 temp_file_map[FILE_MAP_COPY_KEY] = reserved_blocks 287 return temp_file_map 288 289 @staticmethod 290 def get_zero_nonzero_blocks_list(*args): 291 """ 292 Get zero_blocks_list, nonzero_blocks_list, and nonzero_groups_list. 293 data: block data, 294 default_zero_block: default to zero block, 295 each_value: each value, 296 nonzero_blocks_list: nonzero_blocks_list, 297 nonzero_groups_list: nonzero_groups_list, 298 zero_blocks_list: zero_blocks_list, 299 :return new_zero_blocks_list: new zero blocks list, 300 :return new_nonzero_blocks_list: new nonzero blocks list, 301 :return new_nonzero_groups_list: new nonzero groups list. 302 """ 303 data, default_zero_block, each_value, \ 304 nonzero_blocks_list, nonzero_groups_list, \ 305 zero_blocks_list = args 306 # Check whether the data block is equal to the default zero_blocks. 307 if data == default_zero_block: 308 zero_blocks_list.append(each_value) 309 zero_blocks_list.append(each_value + 1) 310 else: 311 nonzero_blocks_list.append(each_value) 312 nonzero_blocks_list.append(each_value + 1) 313 # The number of nonzero_blocks is greater than 314 # or equal to the upper limit. 315 if len(nonzero_blocks_list) >= MAX_BLOCKS_PER_GROUP: 316 nonzero_groups_list.append(nonzero_blocks_list) 317 nonzero_blocks_list = [] 318 new_zero_blocks_list, new_nonzero_blocks_list, \ 319 new_nonzero_groups_list = \ 320 copy.copy(zero_blocks_list), \ 321 copy.copy(nonzero_blocks_list),\ 322 copy.copy(nonzero_groups_list) 323 return new_zero_blocks_list, new_nonzero_blocks_list, \ 324 new_nonzero_groups_list 325 326 @staticmethod 327 def get_file_data(*args): 328 """ 329 Get the file data. 330 block_size: blocksize, 331 chunk_start: the start position of chunk, 332 default_zero_block: default to zero blocks, 333 each_value: each_value, 334 file_pos: file position, 335 fill_data: data, 336 image_file_r: read file object, 337 :return data: Get the file data. 338 """ 339 block_size, chunk_start, default_zero_block, each_value, \ 340 file_pos, fill_data, image_file_r = args 341 if file_pos is not None: 342 file_pos += (each_value - chunk_start) * block_size 343 image_file_r.seek(file_pos, os.SEEK_SET) 344 data = image_file_r.read(block_size) 345 else: 346 if fill_data == default_zero_block[:4]: 347 data = default_zero_block 348 else: 349 data = None 350 return data 351 352 def range_sha256(self, ranges): 353 """ 354 range sha256 hash content 355 :param ranges: ranges value 356 :return: 357 """ 358 hash_obj = sha256() 359 for data in self.__get_blocks_set_data(ranges): 360 hash_obj.update(data) 361 return hash_obj.hexdigest() 362 363 def write_range_data_2_fd(self, ranges, file_obj): 364 """ 365 write range data to fd 366 :param ranges: ranges obj 367 :param file_obj: file obj 368 :return: 369 """ 370 for data in self.__get_blocks_set_data(ranges): 371 file_obj.write(data) 372 373 def get_ranges(self, ranges): 374 """ 375 get ranges value 376 :param ranges: ranges 377 :return: ranges value 378 """ 379 return [each_data for each_data in self.__get_blocks_set_data(ranges)] 380 381 def __get_blocks_set_data(self, blocks_set_data): 382 """ 383 Get the range data. 384 """ 385 with open(self.image_path, 'rb') as f_r: 386 for start, end in blocks_set_data: 387 diff_value = end - start 388 idx = bisect.bisect_right(self.offset_index, start) - 1 389 chunk_start, chunk_len, file_pos, fill_data = \ 390 self.offset_value_list[idx] 391 392 remain = chunk_len - (start - chunk_start) 393 this_read = min(remain, diff_value) 394 if file_pos is not None: 395 pos = file_pos + ((start - chunk_start) * self.block_size) 396 f_r.seek(pos, os.SEEK_SET) 397 yield f_r.read(this_read * self.block_size) 398 else: 399 yield fill_data * (this_read * (self.block_size >> 2)) 400 diff_value -= this_read 401 402 while diff_value > 0: 403 idx += 1 404 chunk_start, chunk_len, file_pos, fill_data = \ 405 self.offset_value_list[idx] 406 this_read = min(chunk_len, diff_value) 407 if file_pos is not None: 408 f_r.seek(file_pos, os.SEEK_SET) 409 yield f_r.read(this_read * self.block_size) 410 else: 411 yield fill_data * (this_read * (self.block_size >> 2)) 412 diff_value -= this_read 413 414 @staticmethod 415 def image_header_info_check(header_info): 416 """ 417 Check for new messages of the header_info image. 418 :param header_info: header_info 419 :return: 420 """ 421 image_flag = True 422 # Sparse mirroring header ID. The magic value is fixed to 0xED26FF3A. 423 magic_info = header_info[0] 424 # major version number 425 major_version = header_info[1] 426 # minor version number 427 minor_version = header_info[2] 428 # Length of the header information. 429 # The value is fixed to 28 characters. 430 header_info_size = header_info[3] 431 # Header information size of the chunk. 432 # The length is fixed to 12 characters. 433 chunk_header_info_size = header_info[4] 434 # Number of bytes of a block. The default size is 4096. 435 block_size = header_info[5] 436 # Total number of blocks contained in the current image 437 # (number of blocks in a non-sparse image) 438 total_blocks = header_info[6] 439 # Total number of chunks contained in the current image 440 total_chunks = header_info[7] 441 if magic_info != SPARSE_IMAGE_MAGIC: 442 UPDATE_LOGGER.print_log( 443 "SparseImage head Magic should be 0xED26FF3A!") 444 image_flag = False 445 if major_version != 1 or minor_version != 0: 446 UPDATE_LOGGER.print_log( 447 "SparseImage Only supported major version with " 448 "minor version 1.0!") 449 image_flag = False 450 if header_info_size != 28: 451 UPDATE_LOGGER.print_log( 452 "SparseImage header info size must be 28! size: %u." % 453 header_info_size) 454 image_flag = False 455 if chunk_header_info_size != 12: 456 UPDATE_LOGGER.print_log( 457 "SparseImage Chunk header size mast to be 12! size: %u." % 458 chunk_header_info_size) 459 image_flag = False 460 ret_args = [block_size, chunk_header_info_size, header_info_size, 461 magic_info, total_blocks, total_chunks, image_flag] 462 return ret_args 463