1# SPDX-License-Identifier:      GPL-2.0+
2# Copyright (c) 2018 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4
5"""Entry-type module for sections (groups of entries)
6
7Sections are entries which can contain other entries. This allows hierarchical
8images to be created.
9"""
10
11from collections import OrderedDict
12import concurrent.futures
13import re
14import sys
15
16from binman.entry import Entry
17from binman import state
18from dtoc import fdt_util
19from patman import tools
20from patman import tout
21from patman.tools import ToHexSize
22
23
24class Entry_section(Entry):
25    """Entry that contains other entries
26
27    Properties / Entry arguments: (see binman README for more information):
28        pad-byte: Pad byte to use when padding
29        sort-by-offset: True if entries should be sorted by offset, False if
30        they must be in-order in the device tree description
31
32        end-at-4gb: Used to build an x86 ROM which ends at 4GB (2^32)
33
34        skip-at-start: Number of bytes before the first entry starts. These
35            effectively adjust the starting offset of entries. For example,
36            if this is 16, then the first entry would start at 16. An entry
37            with offset = 20 would in fact be written at offset 4 in the image
38            file, since the first 16 bytes are skipped when writing.
39        name-prefix: Adds a prefix to the name of every entry in the section
40            when writing out the map
41        align_default: Default alignment for this section, if no alignment is
42            given in the entry
43
44    Properties:
45        allow_missing: True if this section permits external blobs to be
46            missing their contents. The second will produce an image but of
47            course it will not work.
48
49    Since a section is also an entry, it inherits all the properies of entries
50    too.
51
52    A section is an entry which can contain other entries, thus allowing
53    hierarchical images to be created. See 'Sections and hierarchical images'
54    in the binman README for more information.
55    """
56    def __init__(self, section, etype, node, test=False):
57        if not test:
58            super().__init__(section, etype, node)
59        self._entries = OrderedDict()
60        self._pad_byte = 0
61        self._sort = False
62        self._skip_at_start = None
63        self._end_4gb = False
64
65    def ReadNode(self):
66        """Read properties from the section node"""
67        super().ReadNode()
68        self._pad_byte = fdt_util.GetInt(self._node, 'pad-byte', 0)
69        self._sort = fdt_util.GetBool(self._node, 'sort-by-offset')
70        self._end_4gb = fdt_util.GetBool(self._node, 'end-at-4gb')
71        self._skip_at_start = fdt_util.GetInt(self._node, 'skip-at-start')
72        if self._end_4gb:
73            if not self.size:
74                self.Raise("Section size must be provided when using end-at-4gb")
75            if self._skip_at_start is not None:
76                self.Raise("Provide either 'end-at-4gb' or 'skip-at-start'")
77            else:
78                self._skip_at_start = 0x100000000 - self.size
79        else:
80            if self._skip_at_start is None:
81                self._skip_at_start = 0
82        self._name_prefix = fdt_util.GetString(self._node, 'name-prefix')
83        self.align_default = fdt_util.GetInt(self._node, 'align-default', 0)
84        filename = fdt_util.GetString(self._node, 'filename')
85        if filename:
86            self._filename = filename
87
88        self._ReadEntries()
89
90    def _ReadEntries(self):
91        for node in self._node.subnodes:
92            if node.name.startswith('hash') or node.name.startswith('signature'):
93                continue
94            entry = Entry.Create(self, node,
95                                 expanded=self.GetImage().use_expanded)
96            entry.ReadNode()
97            entry.SetPrefix(self._name_prefix)
98            self._entries[node.name] = entry
99
100    def _Raise(self, msg):
101        """Raises an error for this section
102
103        Args:
104            msg: Error message to use in the raise string
105        Raises:
106            ValueError()
107        """
108        raise ValueError("Section '%s': %s" % (self._node.path, msg))
109
110    def GetFdts(self):
111        fdts = {}
112        for entry in self._entries.values():
113            fdts.update(entry.GetFdts())
114        return fdts
115
116    def ProcessFdt(self, fdt):
117        """Allow entries to adjust the device tree
118
119        Some entries need to adjust the device tree for their purposes. This
120        may involve adding or deleting properties.
121        """
122        todo = self._entries.values()
123        for passnum in range(3):
124            next_todo = []
125            for entry in todo:
126                if not entry.ProcessFdt(fdt):
127                    next_todo.append(entry)
128            todo = next_todo
129            if not todo:
130                break
131        if todo:
132            self.Raise('Internal error: Could not complete processing of Fdt: remaining %s' %
133                       todo)
134        return True
135
136    def ExpandEntries(self):
137        super().ExpandEntries()
138        for entry in self._entries.values():
139            entry.ExpandEntries()
140
141    def AddMissingProperties(self, have_image_pos):
142        """Add new properties to the device tree as needed for this entry"""
143        super().AddMissingProperties(have_image_pos)
144        if self.compress != 'none':
145            have_image_pos = False
146        for entry in self._entries.values():
147            entry.AddMissingProperties(have_image_pos)
148
149    def ObtainContents(self):
150        return self.GetEntryContents()
151
152    def GetPaddedDataForEntry(self, entry, entry_data):
153        """Get the data for an entry including any padding
154
155        Gets the entry data and uses the section pad-byte value to add padding
156        before and after as defined by the pad-before and pad-after properties.
157        This does not consider alignment.
158
159        Args:
160            entry: Entry to check
161
162        Returns:
163            Contents of the entry along with any pad bytes before and
164            after it (bytes)
165        """
166        pad_byte = (entry._pad_byte if isinstance(entry, Entry_section)
167                    else self._pad_byte)
168
169        data = bytearray()
170        # Handle padding before the entry
171        if entry.pad_before:
172            data += tools.GetBytes(self._pad_byte, entry.pad_before)
173
174        # Add in the actual entry data
175        data += entry_data
176
177        # Handle padding after the entry
178        if entry.pad_after:
179            data += tools.GetBytes(self._pad_byte, entry.pad_after)
180
181        if entry.size:
182            data += tools.GetBytes(pad_byte, entry.size - len(data))
183
184        self.Detail('GetPaddedDataForEntry: size %s' % ToHexSize(self.data))
185
186        return data
187
188    def _BuildSectionData(self, required):
189        """Build the contents of a section
190
191        This places all entries at the right place, dealing with padding before
192        and after entries. It does not do padding for the section itself (the
193        pad-before and pad-after properties in the section items) since that is
194        handled by the parent section.
195
196        Args:
197            required: True if the data must be present, False if it is OK to
198                return None
199
200        Returns:
201            Contents of the section (bytes)
202        """
203        section_data = bytearray()
204
205        for entry in self._entries.values():
206            entry_data = entry.GetData(required)
207            if not required and entry_data is None:
208                return None
209            data = self.GetPaddedDataForEntry(entry, entry_data)
210            # Handle empty space before the entry
211            pad = (entry.offset or 0) - self._skip_at_start - len(section_data)
212            if pad > 0:
213                section_data += tools.GetBytes(self._pad_byte, pad)
214
215            # Add in the actual entry data
216            section_data += data
217
218        self.Detail('GetData: %d entries, total size %#x' %
219                    (len(self._entries), len(section_data)))
220        return self.CompressData(section_data)
221
222    def GetPaddedData(self, data=None):
223        """Get the data for a section including any padding
224
225        Gets the section data and uses the parent section's pad-byte value to
226        add padding before and after as defined by the pad-before and pad-after
227        properties. If this is a top-level section (i.e. an image), this is the
228        same as GetData(), since padding is not supported.
229
230        This does not consider alignment.
231
232        Returns:
233            Contents of the section along with any pad bytes before and
234            after it (bytes)
235        """
236        section = self.section or self
237        if data is None:
238            data = self.GetData()
239        return section.GetPaddedDataForEntry(self, data)
240
241    def GetData(self, required=True):
242        """Get the contents of an entry
243
244        This builds the contents of the section, stores this as the contents of
245        the section and returns it
246
247        Args:
248            required: True if the data must be present, False if it is OK to
249                return None
250
251        Returns:
252            bytes content of the section, made up for all all of its subentries.
253            This excludes any padding. If the section is compressed, the
254            compressed data is returned
255        """
256        data = self._BuildSectionData(required)
257        if data is None:
258            return None
259        self.SetContents(data)
260        return data
261
262    def GetOffsets(self):
263        """Handle entries that want to set the offset/size of other entries
264
265        This calls each entry's GetOffsets() method. If it returns a list
266        of entries to update, it updates them.
267        """
268        self.GetEntryOffsets()
269        return {}
270
271    def ResetForPack(self):
272        """Reset offset/size fields so that packing can be done again"""
273        super().ResetForPack()
274        for entry in self._entries.values():
275            entry.ResetForPack()
276
277    def Pack(self, offset):
278        """Pack all entries into the section"""
279        self._PackEntries()
280        if self._sort:
281            self._SortEntries()
282        self._ExpandEntries()
283
284        data = self._BuildSectionData(True)
285        self.SetContents(data)
286
287        self.CheckSize()
288
289        offset = super().Pack(offset)
290        self.CheckEntries()
291        return offset
292
293    def _PackEntries(self):
294        """Pack all entries into the section"""
295        offset = self._skip_at_start
296        for entry in self._entries.values():
297            offset = entry.Pack(offset)
298        return offset
299
300    def _ExpandEntries(self):
301        """Expand any entries that are permitted to"""
302        exp_entry = None
303        for entry in self._entries.values():
304            if exp_entry:
305                exp_entry.ExpandToLimit(entry.offset)
306                exp_entry = None
307            if entry.expand_size:
308                exp_entry = entry
309        if exp_entry:
310            exp_entry.ExpandToLimit(self.size)
311
312    def _SortEntries(self):
313        """Sort entries by offset"""
314        entries = sorted(self._entries.values(), key=lambda entry: entry.offset)
315        self._entries.clear()
316        for entry in entries:
317            self._entries[entry._node.name] = entry
318
319    def CheckEntries(self):
320        """Check that entries do not overlap or extend outside the section"""
321        max_size = self.size if self.uncomp_size is None else self.uncomp_size
322
323        offset = 0
324        prev_name = 'None'
325        for entry in self._entries.values():
326            entry.CheckEntries()
327            if (entry.offset < self._skip_at_start or
328                    entry.offset + entry.size > self._skip_at_start +
329                    max_size):
330                entry.Raise('Offset %#x (%d) size %#x (%d) is outside the '
331                            "section '%s' starting at %#x (%d) "
332                            'of size %#x (%d)' %
333                            (entry.offset, entry.offset, entry.size, entry.size,
334                             self._node.path, self._skip_at_start,
335                             self._skip_at_start, max_size, max_size))
336            if entry.offset < offset and entry.size:
337                entry.Raise("Offset %#x (%d) overlaps with previous entry '%s' "
338                            "ending at %#x (%d)" %
339                            (entry.offset, entry.offset, prev_name, offset, offset))
340            offset = entry.offset + entry.size
341            prev_name = entry.GetPath()
342
343    def WriteSymbols(self, section):
344        """Write symbol values into binary files for access at run time"""
345        for entry in self._entries.values():
346            entry.WriteSymbols(self)
347
348    def SetCalculatedProperties(self):
349        super().SetCalculatedProperties()
350        for entry in self._entries.values():
351            entry.SetCalculatedProperties()
352
353    def SetImagePos(self, image_pos):
354        super().SetImagePos(image_pos)
355        if self.compress == 'none':
356            for entry in self._entries.values():
357                entry.SetImagePos(image_pos + self.offset)
358
359    def ProcessContents(self):
360        sizes_ok_base = super(Entry_section, self).ProcessContents()
361        sizes_ok = True
362        for entry in self._entries.values():
363            if not entry.ProcessContents():
364                sizes_ok = False
365        return sizes_ok and sizes_ok_base
366
367    def WriteMap(self, fd, indent):
368        """Write a map of the section to a .map file
369
370        Args:
371            fd: File to write the map to
372        """
373        Entry.WriteMapLine(fd, indent, self.name, self.offset or 0,
374                           self.size, self.image_pos)
375        for entry in self._entries.values():
376            entry.WriteMap(fd, indent + 1)
377
378    def GetEntries(self):
379        return self._entries
380
381    def GetContentsByPhandle(self, phandle, source_entry, required):
382        """Get the data contents of an entry specified by a phandle
383
384        This uses a phandle to look up a node and and find the entry
385        associated with it. Then it returns the contents of that entry.
386
387        The node must be a direct subnode of this section.
388
389        Args:
390            phandle: Phandle to look up (integer)
391            source_entry: Entry containing that phandle (used for error
392                reporting)
393            required: True if the data must be present, False if it is OK to
394                return None
395
396        Returns:
397            data from associated entry (as a string), or None if not found
398        """
399        node = self._node.GetFdt().LookupPhandle(phandle)
400        if not node:
401            source_entry.Raise("Cannot find node for phandle %d" % phandle)
402        for entry in self._entries.values():
403            if entry._node == node:
404                return entry.GetData(required)
405        source_entry.Raise("Cannot find entry for node '%s'" % node.name)
406
407    def LookupSymbol(self, sym_name, optional, msg, base_addr, entries=None):
408        """Look up a symbol in an ELF file
409
410        Looks up a symbol in an ELF file. Only entry types which come from an
411        ELF image can be used by this function.
412
413        At present the only entry properties supported are:
414            offset
415            image_pos - 'base_addr' is added if this is not an end-at-4gb image
416            size
417
418        Args:
419            sym_name: Symbol name in the ELF file to look up in the format
420                _binman_<entry>_prop_<property> where <entry> is the name of
421                the entry and <property> is the property to find (e.g.
422                _binman_u_boot_prop_offset). As a special case, you can append
423                _any to <entry> to have it search for any matching entry. E.g.
424                _binman_u_boot_any_prop_offset will match entries called u-boot,
425                u-boot-img and u-boot-nodtb)
426            optional: True if the symbol is optional. If False this function
427                will raise if the symbol is not found
428            msg: Message to display if an error occurs
429            base_addr: Base address of image. This is added to the returned
430                image_pos in most cases so that the returned position indicates
431                where the targetted entry/binary has actually been loaded. But
432                if end-at-4gb is used, this is not done, since the binary is
433                already assumed to be linked to the ROM position and using
434                execute-in-place (XIP).
435
436        Returns:
437            Value that should be assigned to that symbol, or None if it was
438                optional and not found
439
440        Raises:
441            ValueError if the symbol is invalid or not found, or references a
442                property which is not supported
443        """
444        m = re.match(r'^_binman_(\w+)_prop_(\w+)$', sym_name)
445        if not m:
446            raise ValueError("%s: Symbol '%s' has invalid format" %
447                             (msg, sym_name))
448        entry_name, prop_name = m.groups()
449        entry_name = entry_name.replace('_', '-')
450        if not entries:
451            entries = self._entries
452        entry = entries.get(entry_name)
453        if not entry:
454            if entry_name.endswith('-any'):
455                root = entry_name[:-4]
456                for name in entries:
457                    if name.startswith(root):
458                        rest = name[len(root):]
459                        if rest in ['', '-img', '-nodtb']:
460                            entry = entries[name]
461        if not entry:
462            err = ("%s: Entry '%s' not found in list (%s)" %
463                   (msg, entry_name, ','.join(entries.keys())))
464            if optional:
465                print('Warning: %s' % err, file=sys.stderr)
466                return None
467            raise ValueError(err)
468        if prop_name == 'offset':
469            return entry.offset
470        elif prop_name == 'image_pos':
471            value = entry.image_pos
472            if not self.GetImage()._end_4gb:
473                value += base_addr
474            return value
475        if prop_name == 'size':
476            return entry.size
477        else:
478            raise ValueError("%s: No such property '%s'" % (msg, prop_name))
479
480    def GetRootSkipAtStart(self):
481        """Get the skip-at-start value for the top-level section
482
483        This is used to find out the starting offset for root section that
484        contains this section. If this is a top-level section then it returns
485        the skip-at-start offset for this section.
486
487        This is used to get the absolute position of section within the image.
488
489        Returns:
490            Integer skip-at-start value for the root section containing this
491                section
492        """
493        if self.section:
494            return self.section.GetRootSkipAtStart()
495        return self._skip_at_start
496
497    def GetStartOffset(self):
498        """Get the start offset for this section
499
500        Returns:
501            The first available offset in this section (typically 0)
502        """
503        return self._skip_at_start
504
505    def GetImageSize(self):
506        """Get the size of the image containing this section
507
508        Returns:
509            Image size as an integer number of bytes, which may be None if the
510                image size is dynamic and its sections have not yet been packed
511        """
512        return self.GetImage().size
513
514    def FindEntryType(self, etype):
515        """Find an entry type in the section
516
517        Args:
518            etype: Entry type to find
519        Returns:
520            entry matching that type, or None if not found
521        """
522        for entry in self._entries.values():
523            if entry.etype == etype:
524                return entry
525        return None
526
527    def GetEntryContents(self):
528        """Call ObtainContents() for each entry in the section
529        """
530        def _CheckDone(entry):
531            if not entry.ObtainContents():
532                next_todo.append(entry)
533            return entry
534
535        todo = self._entries.values()
536        for passnum in range(3):
537            threads = state.GetThreads()
538            next_todo = []
539
540            if threads == 0:
541                for entry in todo:
542                    _CheckDone(entry)
543            else:
544                with concurrent.futures.ThreadPoolExecutor(
545                        max_workers=threads) as executor:
546                    future_to_data = {
547                        entry: executor.submit(_CheckDone, entry)
548                        for entry in todo}
549                    timeout = 60
550                    if self.GetImage().test_section_timeout:
551                        timeout = 0
552                    done, not_done = concurrent.futures.wait(
553                        future_to_data.values(), timeout=timeout)
554                    # Make sure we check the result, so any exceptions are
555                    # generated. Check the results in entry order, since tests
556                    # may expect earlier entries to fail first.
557                    for entry in todo:
558                        job = future_to_data[entry]
559                        job.result()
560                    if not_done:
561                        self.Raise('Timed out obtaining contents')
562
563            todo = next_todo
564            if not todo:
565                break
566
567        if todo:
568            self.Raise('Internal error: Could not complete processing of contents: remaining %s' %
569                       todo)
570        return True
571
572    def _SetEntryOffsetSize(self, name, offset, size):
573        """Set the offset and size of an entry
574
575        Args:
576            name: Entry name to update
577            offset: New offset, or None to leave alone
578            size: New size, or None to leave alone
579        """
580        entry = self._entries.get(name)
581        if not entry:
582            self._Raise("Unable to set offset/size for unknown entry '%s'" %
583                        name)
584        entry.SetOffsetSize(self._skip_at_start + offset if offset is not None
585                            else None, size)
586
587    def GetEntryOffsets(self):
588        """Handle entries that want to set the offset/size of other entries
589
590        This calls each entry's GetOffsets() method. If it returns a list
591        of entries to update, it updates them.
592        """
593        for entry in self._entries.values():
594            offset_dict = entry.GetOffsets()
595            for name, info in offset_dict.items():
596                self._SetEntryOffsetSize(name, *info)
597
598    def CheckSize(self):
599        contents_size = len(self.data)
600
601        size = self.size
602        if not size:
603            data = self.GetPaddedData(self.data)
604            size = len(data)
605            size = tools.Align(size, self.align_size)
606
607        if self.size and contents_size > self.size:
608            self._Raise("contents size %#x (%d) exceeds section size %#x (%d)" %
609                        (contents_size, contents_size, self.size, self.size))
610        if not self.size:
611            self.size = size
612        if self.size != tools.Align(self.size, self.align_size):
613            self._Raise("Size %#x (%d) does not match align-size %#x (%d)" %
614                        (self.size, self.size, self.align_size,
615                         self.align_size))
616        return size
617
618    def ListEntries(self, entries, indent):
619        """List the files in the section"""
620        Entry.AddEntryInfo(entries, indent, self.name, 'section', self.size,
621                           self.image_pos, None, self.offset, self)
622        for entry in self._entries.values():
623            entry.ListEntries(entries, indent + 1)
624
625    def LoadData(self, decomp=True):
626        for entry in self._entries.values():
627            entry.LoadData(decomp)
628        self.Detail('Loaded data')
629
630    def GetImage(self):
631        """Get the image containing this section
632
633        Note that a top-level section is actually an Image, so this function may
634        return self.
635
636        Returns:
637            Image object containing this section
638        """
639        if not self.section:
640            return self
641        return self.section.GetImage()
642
643    def GetSort(self):
644        """Check if the entries in this section will be sorted
645
646        Returns:
647            True if to be sorted, False if entries will be left in the order
648                they appear in the device tree
649        """
650        return self._sort
651
652    def ReadData(self, decomp=True):
653        tout.Info("ReadData path='%s'" % self.GetPath())
654        parent_data = self.section.ReadData(True)
655        offset = self.offset - self.section._skip_at_start
656        data = parent_data[offset:offset + self.size]
657        tout.Info(
658            '%s: Reading data from offset %#x-%#x (real %#x), size %#x, got %#x' %
659                  (self.GetPath(), self.offset, self.offset + self.size, offset,
660                   self.size, len(data)))
661        return data
662
663    def ReadChildData(self, child, decomp=True):
664        tout.Debug("ReadChildData for child '%s'" % child.GetPath())
665        parent_data = self.ReadData(True)
666        offset = child.offset - self._skip_at_start
667        tout.Debug("Extract for child '%s': offset %#x, skip_at_start %#x, result %#x" %
668                   (child.GetPath(), child.offset, self._skip_at_start, offset))
669        data = parent_data[offset:offset + child.size]
670        if decomp:
671            indata = data
672            data = tools.Decompress(indata, child.compress)
673            if child.uncomp_size:
674                tout.Info("%s: Decompressing data size %#x with algo '%s' to data size %#x" %
675                            (child.GetPath(), len(indata), child.compress,
676                            len(data)))
677        return data
678
679    def WriteChildData(self, child):
680        return True
681
682    def SetAllowMissing(self, allow_missing):
683        """Set whether a section allows missing external blobs
684
685        Args:
686            allow_missing: True if allowed, False if not allowed
687        """
688        self.allow_missing = allow_missing
689        for entry in self._entries.values():
690            entry.SetAllowMissing(allow_missing)
691
692    def CheckMissing(self, missing_list):
693        """Check if any entries in this section have missing external blobs
694
695        If there are missing blobs, the entries are added to the list
696
697        Args:
698            missing_list: List of Entry objects to be added to
699        """
700        for entry in self._entries.values():
701            entry.CheckMissing(missing_list)
702
703    def _CollectEntries(self, entries, entries_by_name, add_entry):
704        """Collect all the entries in an section
705
706        This builds up a dict of entries in this section and all subsections.
707        Entries are indexed by path and by name.
708
709        Since all paths are unique, entries will not have any conflicts. However
710        entries_by_name make have conflicts if two entries have the same name
711        (e.g. with different parent sections). In this case, an entry at a
712        higher level in the hierarchy will win over a lower-level entry.
713
714        Args:
715            entries: dict to put entries:
716                key: entry path
717                value: Entry object
718            entries_by_name: dict to put entries
719                key: entry name
720                value: Entry object
721            add_entry: Entry to add
722        """
723        entries[add_entry.GetPath()] = add_entry
724        to_add = add_entry.GetEntries()
725        if to_add:
726            for entry in to_add.values():
727                entries[entry.GetPath()] = entry
728            for entry in to_add.values():
729                self._CollectEntries(entries, entries_by_name, entry)
730        entries_by_name[add_entry.name] = add_entry
731
732    def MissingArgs(self, entry, missing):
733        """Report a missing argument, if enabled
734
735        For entries which require arguments, this reports an error if some are
736        missing. If missing entries are being ignored (e.g. because we read the
737        entry from an image rather than creating it), this function does
738        nothing.
739
740        Args:
741            missing: List of missing properties / entry args, each a string
742        """
743        if not self._ignore_missing:
744            entry.Raise('Missing required properties/entry args: %s' %
745                       (', '.join(missing)))
746