1# SPDX-License-Identifier: GPL-2.0+ 2# 3# Copyright (c) 2016 Google, Inc 4# 5 6from __future__ import print_function 7 8import command 9import glob 10import os 11import shutil 12import struct 13import sys 14import tempfile 15 16import tout 17 18# Output directly (generally this is temporary) 19outdir = None 20 21# True to keep the output directory around after exiting 22preserve_outdir = False 23 24# Path to the Chrome OS chroot, if we know it 25chroot_path = None 26 27# Search paths to use for Filename(), used to find files 28search_paths = [] 29 30tool_search_paths = [] 31 32# Tools and the packages that contain them, on debian 33packages = { 34 'lz4': 'liblz4-tool', 35 } 36 37# List of paths to use when looking for an input file 38indir = [] 39 40def PrepareOutputDir(dirname, preserve=False): 41 """Select an output directory, ensuring it exists. 42 43 This either creates a temporary directory or checks that the one supplied 44 by the user is valid. For a temporary directory, it makes a note to 45 remove it later if required. 46 47 Args: 48 dirname: a string, name of the output directory to use to store 49 intermediate and output files. If is None - create a temporary 50 directory. 51 preserve: a Boolean. If outdir above is None and preserve is False, the 52 created temporary directory will be destroyed on exit. 53 54 Raises: 55 OSError: If it cannot create the output directory. 56 """ 57 global outdir, preserve_outdir 58 59 preserve_outdir = dirname or preserve 60 if dirname: 61 outdir = dirname 62 if not os.path.isdir(outdir): 63 try: 64 os.makedirs(outdir) 65 except OSError as err: 66 raise CmdError("Cannot make output directory '%s': '%s'" % 67 (outdir, err.strerror)) 68 tout.Debug("Using output directory '%s'" % outdir) 69 else: 70 outdir = tempfile.mkdtemp(prefix='binman.') 71 tout.Debug("Using temporary directory '%s'" % outdir) 72 73def _RemoveOutputDir(): 74 global outdir 75 76 shutil.rmtree(outdir) 77 tout.Debug("Deleted temporary directory '%s'" % outdir) 78 outdir = None 79 80def FinaliseOutputDir(): 81 global outdir, preserve_outdir 82 83 """Tidy up: delete output directory if temporary and not preserved.""" 84 if outdir and not preserve_outdir: 85 _RemoveOutputDir() 86 outdir = None 87 88def GetOutputFilename(fname): 89 """Return a filename within the output directory. 90 91 Args: 92 fname: Filename to use for new file 93 94 Returns: 95 The full path of the filename, within the output directory 96 """ 97 return os.path.join(outdir, fname) 98 99def _FinaliseForTest(): 100 """Remove the output directory (for use by tests)""" 101 global outdir 102 103 if outdir: 104 _RemoveOutputDir() 105 outdir = None 106 107def SetInputDirs(dirname): 108 """Add a list of input directories, where input files are kept. 109 110 Args: 111 dirname: a list of paths to input directories to use for obtaining 112 files needed by binman to place in the image. 113 """ 114 global indir 115 116 indir = dirname 117 tout.Debug("Using input directories %s" % indir) 118 119def GetInputFilename(fname): 120 """Return a filename for use as input. 121 122 Args: 123 fname: Filename to use for new file 124 125 Returns: 126 The full path of the filename, within the input directory 127 """ 128 if not indir or fname[:1] == '/': 129 return fname 130 for dirname in indir: 131 pathname = os.path.join(dirname, fname) 132 if os.path.exists(pathname): 133 return pathname 134 135 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" % 136 (fname, ','.join(indir), os.getcwd())) 137 138def GetInputFilenameGlob(pattern): 139 """Return a list of filenames for use as input. 140 141 Args: 142 pattern: Filename pattern to search for 143 144 Returns: 145 A list of matching files in all input directories 146 """ 147 if not indir: 148 return glob.glob(fname) 149 files = [] 150 for dirname in indir: 151 pathname = os.path.join(dirname, pattern) 152 files += glob.glob(pathname) 153 return sorted(files) 154 155def Align(pos, align): 156 if align: 157 mask = align - 1 158 pos = (pos + mask) & ~mask 159 return pos 160 161def NotPowerOfTwo(num): 162 return num and (num & (num - 1)) 163 164def SetToolPaths(toolpaths): 165 """Set the path to search for tools 166 167 Args: 168 toolpaths: List of paths to search for tools executed by Run() 169 """ 170 global tool_search_paths 171 172 tool_search_paths = toolpaths 173 174def PathHasFile(path_spec, fname): 175 """Check if a given filename is in the PATH 176 177 Args: 178 path_spec: Value of PATH variable to check 179 fname: Filename to check 180 181 Returns: 182 True if found, False if not 183 """ 184 for dir in path_spec.split(':'): 185 if os.path.exists(os.path.join(dir, fname)): 186 return True 187 return False 188 189def Run(name, *args, **kwargs): 190 """Run a tool with some arguments 191 192 This runs a 'tool', which is a program used by binman to process files and 193 perhaps produce some output. Tools can be located on the PATH or in a 194 search path. 195 196 Args: 197 name: Command name to run 198 args: Arguments to the tool 199 200 Returns: 201 CommandResult object 202 """ 203 try: 204 binary = kwargs.get('binary') 205 env = None 206 if tool_search_paths: 207 env = dict(os.environ) 208 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH'] 209 all_args = (name,) + args 210 result = command.RunPipe([all_args], capture=True, capture_stderr=True, 211 env=env, raise_on_error=False, binary=binary) 212 if result.return_code: 213 raise Exception("Error %d running '%s': %s" % 214 (result.return_code,' '.join(all_args), 215 result.stderr)) 216 return result.stdout 217 except: 218 if env and not PathHasFile(env['PATH'], name): 219 msg = "Please install tool '%s'" % name 220 package = packages.get(name) 221 if package: 222 msg += " (e.g. from package '%s')" % package 223 raise ValueError(msg) 224 raise 225 226def Filename(fname): 227 """Resolve a file path to an absolute path. 228 229 If fname starts with ##/ and chroot is available, ##/ gets replaced with 230 the chroot path. If chroot is not available, this file name can not be 231 resolved, `None' is returned. 232 233 If fname is not prepended with the above prefix, and is not an existing 234 file, the actual file name is retrieved from the passed in string and the 235 search_paths directories (if any) are searched to for the file. If found - 236 the path to the found file is returned, `None' is returned otherwise. 237 238 Args: 239 fname: a string, the path to resolve. 240 241 Returns: 242 Absolute path to the file or None if not found. 243 """ 244 if fname.startswith('##/'): 245 if chroot_path: 246 fname = os.path.join(chroot_path, fname[3:]) 247 else: 248 return None 249 250 # Search for a pathname that exists, and return it if found 251 if fname and not os.path.exists(fname): 252 for path in search_paths: 253 pathname = os.path.join(path, os.path.basename(fname)) 254 if os.path.exists(pathname): 255 return pathname 256 257 # If not found, just return the standard, unchanged path 258 return fname 259 260def ReadFile(fname, binary=True): 261 """Read and return the contents of a file. 262 263 Args: 264 fname: path to filename to read, where ## signifiies the chroot. 265 266 Returns: 267 data read from file, as a string. 268 """ 269 with open(Filename(fname), binary and 'rb' or 'r') as fd: 270 data = fd.read() 271 #self._out.Info("Read file '%s' size %d (%#0x)" % 272 #(fname, len(data), len(data))) 273 return data 274 275def WriteFile(fname, data): 276 """Write data into a file. 277 278 Args: 279 fname: path to filename to write 280 data: data to write to file, as a string 281 """ 282 #self._out.Info("Write file '%s' size %d (%#0x)" % 283 #(fname, len(data), len(data))) 284 with open(Filename(fname), 'wb') as fd: 285 fd.write(data) 286 287def GetBytes(byte, size): 288 """Get a string of bytes of a given size 289 290 This handles the unfortunate different between Python 2 and Python 2. 291 292 Args: 293 byte: Numeric byte value to use 294 size: Size of bytes/string to return 295 296 Returns: 297 A bytes type with 'byte' repeated 'size' times 298 """ 299 if sys.version_info[0] >= 3: 300 data = bytes([byte]) * size 301 else: 302 data = chr(byte) * size 303 return data 304 305def ToUnicode(val): 306 """Make sure a value is a unicode string 307 308 This allows some amount of compatibility between Python 2 and Python3. For 309 the former, it returns a unicode object. 310 311 Args: 312 val: string or unicode object 313 314 Returns: 315 unicode version of val 316 """ 317 if sys.version_info[0] >= 3: 318 return val 319 return val if isinstance(val, unicode) else val.decode('utf-8') 320 321def FromUnicode(val): 322 """Make sure a value is a non-unicode string 323 324 This allows some amount of compatibility between Python 2 and Python3. For 325 the former, it converts a unicode object to a string. 326 327 Args: 328 val: string or unicode object 329 330 Returns: 331 non-unicode version of val 332 """ 333 if sys.version_info[0] >= 3: 334 return val 335 return val if isinstance(val, str) else val.encode('utf-8') 336 337def ToByte(ch): 338 """Convert a character to an ASCII value 339 340 This is useful because in Python 2 bytes is an alias for str, but in 341 Python 3 they are separate types. This function converts the argument to 342 an ASCII value in either case. 343 344 Args: 345 ch: A string (Python 2) or byte (Python 3) value 346 347 Returns: 348 integer ASCII value for ch 349 """ 350 return ord(ch) if type(ch) == str else ch 351 352def ToChar(byte): 353 """Convert a byte to a character 354 355 This is useful because in Python 2 bytes is an alias for str, but in 356 Python 3 they are separate types. This function converts an ASCII value to 357 a value with the appropriate type in either case. 358 359 Args: 360 byte: A byte or str value 361 """ 362 return chr(byte) if type(byte) != str else byte 363 364def ToChars(byte_list): 365 """Convert a list of bytes to a str/bytes type 366 367 Args: 368 byte_list: List of ASCII values representing the string 369 370 Returns: 371 string made by concatenating all the ASCII values 372 """ 373 return ''.join([chr(byte) for byte in byte_list]) 374 375def ToBytes(string): 376 """Convert a str type into a bytes type 377 378 Args: 379 string: string to convert 380 381 Returns: 382 Python 3: A bytes type 383 Python 2: A string type 384 """ 385 if sys.version_info[0] >= 3: 386 return string.encode('utf-8') 387 return string 388 389def ToString(bval): 390 """Convert a bytes type into a str type 391 392 Args: 393 bval: bytes value to convert 394 395 Returns: 396 Python 3: A bytes type 397 Python 2: A string type 398 """ 399 return bval.decode('utf-8') 400 401def Compress(indata, algo, with_header=True): 402 """Compress some data using a given algorithm 403 404 Note that for lzma this uses an old version of the algorithm, not that 405 provided by xz. 406 407 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 408 directory to be previously set up, by calling PrepareOutputDir(). 409 410 Args: 411 indata: Input data to compress 412 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 413 414 Returns: 415 Compressed data 416 """ 417 if algo == 'none': 418 return indata 419 fname = GetOutputFilename('%s.comp.tmp' % algo) 420 WriteFile(fname, indata) 421 if algo == 'lz4': 422 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True) 423 # cbfstool uses a very old version of lzma 424 elif algo == 'lzma': 425 outfname = GetOutputFilename('%s.comp.otmp' % algo) 426 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8') 427 data = ReadFile(outfname) 428 elif algo == 'gzip': 429 data = Run('gzip', '-c', fname, binary=True) 430 else: 431 raise ValueError("Unknown algorithm '%s'" % algo) 432 if with_header: 433 hdr = struct.pack('<I', len(data)) 434 data = hdr + data 435 return data 436 437def Decompress(indata, algo, with_header=True): 438 """Decompress some data using a given algorithm 439 440 Note that for lzma this uses an old version of the algorithm, not that 441 provided by xz. 442 443 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 444 directory to be previously set up, by calling PrepareOutputDir(). 445 446 Args: 447 indata: Input data to decompress 448 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 449 450 Returns: 451 Compressed data 452 """ 453 if algo == 'none': 454 return indata 455 if with_header: 456 data_len = struct.unpack('<I', indata[:4])[0] 457 indata = indata[4:4 + data_len] 458 fname = GetOutputFilename('%s.decomp.tmp' % algo) 459 with open(fname, 'wb') as fd: 460 fd.write(indata) 461 if algo == 'lz4': 462 data = Run('lz4', '-dc', fname, binary=True) 463 elif algo == 'lzma': 464 outfname = GetOutputFilename('%s.decomp.otmp' % algo) 465 Run('lzma_alone', 'd', fname, outfname) 466 data = ReadFile(outfname, binary=True) 467 elif algo == 'gzip': 468 data = Run('gzip', '-cd', fname, binary=True) 469 else: 470 raise ValueError("Unknown algorithm '%s'" % algo) 471 return data 472 473CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5) 474 475IFWITOOL_CMDS = { 476 CMD_CREATE: 'create', 477 CMD_DELETE: 'delete', 478 CMD_ADD: 'add', 479 CMD_REPLACE: 'replace', 480 CMD_EXTRACT: 'extract', 481 } 482 483def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None): 484 """Run ifwitool with the given arguments: 485 486 Args: 487 ifwi_file: IFWI file to operation on 488 cmd: Command to execute (CMD_...) 489 fname: Filename of file to add/replace/extract/create (None for 490 CMD_DELETE) 491 subpart: Name of sub-partition to operation on (None for CMD_CREATE) 492 entry_name: Name of directory entry to operate on, or None if none 493 """ 494 args = ['ifwitool', ifwi_file] 495 args.append(IFWITOOL_CMDS[cmd]) 496 if fname: 497 args += ['-f', fname] 498 if subpart: 499 args += ['-n', subpart] 500 if entry_name: 501 args += ['-d', '-e', entry_name] 502 Run(*args) 503 504def ToHex(val): 505 """Convert an integer value (or None) to a string 506 507 Returns: 508 hex value, or 'None' if the value is None 509 """ 510 return 'None' if val is None else '%#x' % val 511 512def ToHexSize(val): 513 """Return the size of an object in hex 514 515 Returns: 516 hex value of size, or 'None' if the value is None 517 """ 518 return 'None' if val is None else '%#x' % len(val) 519