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