• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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