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