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 __future__ import annotations
12from collections import OrderedDict
13import concurrent.futures
14import re
15import sys
16
17from binman.entry import Entry
18from binman import state
19from dtoc import fdt_util
20from u_boot_pylib import tools
21from u_boot_pylib import tout
22from u_boot_pylib.tools import to_hex_size
23
24
25class Entry_section(Entry):
26    """Entry that contains other entries
27
28    A section is an entry which can contain other entries, thus allowing
29    hierarchical images to be created. See 'Sections and hierarchical images'
30    in the binman README for more information.
31
32    The base implementation simply joins the various entries together, using
33    various rules about alignment, etc.
34
35    Subclassing
36    ~~~~~~~~~~~
37
38    This class can be subclassed to support other file formats which hold
39    multiple entries, such as CBFS. To do this, override the following
40    functions. The documentation here describes what your function should do.
41    For example code, see etypes which subclass `Entry_section`, or `cbfs.py`
42    for a more involved example::
43
44       $ grep -l \\(Entry_section tools/binman/etype/*.py
45
46    ReadNode()
47        Call `super().ReadNode()`, then read any special properties for the
48        section. Then call `self.ReadEntries()` to read the entries.
49
50        Binman calls this at the start when reading the image description.
51
52    ReadEntries()
53        Read in the subnodes of the section. This may involve creating entries
54        of a particular etype automatically, as well as reading any special
55        properties in the entries. For each entry, entry.ReadNode() should be
56        called, to read the basic entry properties. The properties should be
57        added to `self._entries[]`, in the correct order, with a suitable name.
58
59        Binman calls this at the start when reading the image description.
60
61    BuildSectionData(required)
62        Create the custom file format that you want and return it as bytes.
63        This likely sets up a file header, then loops through the entries,
64        adding them to the file. For each entry, call `entry.GetData()` to
65        obtain the data. If that returns None, and `required` is False, then
66        this method must give up and return None. But if `required` is True then
67        it should assume that all data is valid.
68
69        Binman calls this when packing the image, to find out the size of
70        everything. It is called again at the end when building the final image.
71
72    SetImagePos(image_pos):
73        Call `super().SetImagePos(image_pos)`, then set the `image_pos` values
74        for each of the entries. This should use the custom file format to find
75        the `start offset` (and `image_pos`) of each entry. If the file format
76        uses compression in such a way that there is no offset available (other
77        than reading the whole file and decompressing it), then the offsets for
78        affected entries can remain unset (`None`). The size should also be set
79        if possible.
80
81        Binman calls this after the image has been packed, to update the
82        location that all the entries ended up at.
83
84    ReadChildData(child, decomp, alt_format):
85        The default version of this may be good enough, if you are able to
86        implement SetImagePos() correctly. But that is a bit of a bypass, so
87        you can override this method to read from your custom file format. It
88        should read the entire entry containing the custom file using
89        `super().ReadData(True)`, then parse the file to get the data for the
90        given child, then return that data.
91
92        If your file format supports compression, the `decomp` argument tells
93        you whether to return the compressed data (`decomp` is False) or to
94        uncompress it first, then return the uncompressed data (`decomp` is
95        True). This is used by the `binman extract -U` option.
96
97        If your entry supports alternative formats, the alt_format provides the
98        alternative format that the user has selected. Your function should
99        return data in that format. This is used by the 'binman extract -l'
100        option.
101
102        Binman calls this when reading in an image, in order to populate all the
103        entries with the data from that image (`binman ls`).
104
105    WriteChildData(child):
106        Binman calls this after `child.data` is updated, to inform the custom
107        file format about this, in case it needs to do updates.
108
109        The default version of this does nothing and probably needs to be
110        overridden for the 'binman replace' command to work. Your version should
111        use `child.data` to update the data for that child in the custom file
112        format.
113
114        Binman calls this when updating an image that has been read in and in
115        particular to update the data for a particular entry (`binman replace`)
116
117    Properties / Entry arguments
118    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
119
120    See :ref:`develop/package/binman:Image description format` for more
121    information.
122
123    align-default
124        Default alignment for this section, if no alignment is given in the
125        entry
126
127    pad-byte
128        Pad byte to use when padding
129
130    sort-by-offset
131        True if entries should be sorted by offset, False if they must be
132        in-order in the device tree description
133
134    end-at-4gb
135        Used to build an x86 ROM which ends at 4GB (2^32)
136
137    name-prefix
138        Adds a prefix to the name of every entry in the section when writing out
139        the map
140
141    skip-at-start
142        Number of bytes before the first entry starts. These effectively adjust
143        the starting offset of entries. For example, if this is 16, then the
144        first entry would start at 16. An entry with offset = 20 would in fact
145        be written at offset 4 in the image file, since the first 16 bytes are
146        skipped when writing.
147
148    filename
149        filename to write the unpadded section contents to within the output
150        directory (None to skip this).
151
152    Since a section is also an entry, it inherits all the properies of entries
153    too.
154
155    Note that the `allow_missing` member controls whether this section permits
156    external blobs to be missing their contents. The option will produce an
157    image but of course it will not work. It is useful to make sure that
158    Continuous Integration systems can build without the binaries being
159    available. This is set by the `SetAllowMissing()` method, if
160    `--allow-missing` is passed to binman.
161    """
162    def __init__(self, section, etype, node, test=False):
163        if not test:
164            super().__init__(section, etype, node)
165        self._entries = OrderedDict()
166        self._pad_byte = 0
167        self._sort = False
168        self._skip_at_start = None
169        self._end_at_4gb = False
170        self._ignore_missing = False
171        self._filename = None
172        self.align_default = 0
173
174    def IsSpecialSubnode(self, node):
175        """Check if a node is a special one used by the section itself
176
177        Some nodes are used for hashing / signatures and do not add entries to
178        the actual section.
179
180        Returns:
181            bool: True if the node is a special one, else False
182        """
183        start_list = ('cipher', 'hash', 'signature', 'template')
184        return any(node.name.startswith(name) for name in start_list)
185
186    def ReadNode(self):
187        """Read properties from the section node"""
188        super().ReadNode()
189        self._pad_byte = fdt_util.GetInt(self._node, 'pad-byte', 0)
190        self._sort = fdt_util.GetBool(self._node, 'sort-by-offset')
191        self._end_at_4gb = fdt_util.GetBool(self._node, 'end-at-4gb')
192        self._skip_at_start = fdt_util.GetInt(self._node, 'skip-at-start')
193        if self._end_at_4gb and self.GetImage().copy_to_orig:
194            if not self.size:
195                self.Raise("Section size must be provided when using end-at-4gb")
196            if self._skip_at_start is not None:
197                self.Raise("Provide either 'end-at-4gb' or 'skip-at-start'")
198            else:
199                self._skip_at_start = 0x100000000 - self.size
200        else:
201            if self._skip_at_start is None:
202                self._skip_at_start = 0
203        self._name_prefix = fdt_util.GetString(self._node, 'name-prefix')
204        self.align_default = fdt_util.GetInt(self._node, 'align-default', 0)
205        self._filename = fdt_util.GetString(self._node, 'filename',
206                                            self._filename)
207
208        self.ReadEntries()
209
210    def ReadEntries(self):
211        for node in self._node.subnodes:
212            if self.IsSpecialSubnode(node):
213                continue
214            entry = Entry.Create(self, node,
215                                 expanded=self.GetImage().use_expanded,
216                                 missing_etype=self.GetImage().missing_etype)
217            entry.ReadNode()
218            entry.SetPrefix(self._name_prefix)
219            self._entries[node.name] = entry
220
221    def _Raise(self, msg):
222        """Raises an error for this section
223
224        Args:
225            msg (str): Error message to use in the raise string
226        Raises:
227            ValueError: always
228        """
229        raise ValueError("Section '%s': %s" % (self._node.path, msg))
230
231    def GetFdts(self):
232        fdts = {}
233        for entry in self._entries.values():
234            fdts.update(entry.GetFdts())
235        return fdts
236
237    def ProcessFdt(self, fdt):
238        """Allow entries to adjust the device tree
239
240        Some entries need to adjust the device tree for their purposes. This
241        may involve adding or deleting properties.
242        """
243        todo = self._entries.values()
244        for passnum in range(3):
245            next_todo = []
246            for entry in todo:
247                if not entry.ProcessFdt(fdt):
248                    next_todo.append(entry)
249            todo = next_todo
250            if not todo:
251                break
252        if todo:
253            self.Raise('Internal error: Could not complete processing of Fdt: remaining %s' %
254                       todo)
255        return True
256
257    def gen_entries(self):
258        super().gen_entries()
259        for entry in self._entries.values():
260            entry.gen_entries()
261
262    def AddMissingProperties(self, have_image_pos):
263        """Add new properties to the device tree as needed for this entry"""
264        super().AddMissingProperties(have_image_pos)
265        if self.compress != 'none':
266            have_image_pos = False
267        if self._end_at_4gb:
268            state.AddZeroProp(self._node, 'skip-at-start')
269        for entry in self._entries.values():
270            entry.AddMissingProperties(have_image_pos)
271
272    def ObtainContents(self, fake_size=0, skip_entry=None):
273        return self.GetEntryContents(skip_entry=skip_entry)
274
275    def GetPaddedDataForEntry(self, entry, entry_data):
276        """Get the data for an entry including any padding
277
278        Gets the entry data and uses the section pad-byte value to add padding
279        before and after as defined by the pad-before and pad-after properties.
280        This does not consider alignment.
281
282        Args:
283            entry: Entry to check
284            entry_data: Data for the entry, False if is null
285
286        Returns:
287            Contents of the entry along with any pad bytes before and
288            after it (bytes)
289        """
290        pad_byte = (entry._pad_byte if isinstance(entry, Entry_section)
291                    else self._pad_byte)
292
293        data = bytearray()
294        # Handle padding before the entry
295        if entry.pad_before:
296            data += tools.get_bytes(self._pad_byte, entry.pad_before)
297
298        # Add in the actual entry data
299        data += entry_data
300
301        # Handle padding after the entry
302        if entry.pad_after:
303            data += tools.get_bytes(self._pad_byte, entry.pad_after)
304
305        if entry.size:
306            data += tools.get_bytes(pad_byte, entry.size - len(data))
307
308        self.Detail('GetPaddedDataForEntry: size %s' % to_hex_size(self.data))
309
310        return data
311
312    def BuildSectionData(self, required):
313        """Build the contents of a section
314
315        This places all entries at the right place, dealing with padding before
316        and after entries. It does not do padding for the section itself (the
317        pad-before and pad-after properties in the section items) since that is
318        handled by the parent section.
319
320        This should be overridden by subclasses which want to build their own
321        data structure for the section.
322
323        Missing entries will have be given empty (or fake) data, so are
324        processed normally here.
325
326        Args:
327            required: True if the data must be present, False if it is OK to
328                return None
329
330        Returns:
331            Contents of the section (bytes), None if not available
332        """
333        section_data = bytearray()
334
335        for entry in self._entries.values():
336            entry_data = entry.GetData(required)
337
338            # This can happen when this section is referenced from a collection
339            # earlier in the image description. See testCollectionSection().
340            if not required and entry_data is None:
341                return None
342
343            entry_data_final = entry_data
344            if entry_data is None:
345                pad_byte = (entry._pad_byte if isinstance(entry, Entry_section)
346                            else self._pad_byte)
347                entry_data_final = tools.get_bytes(self._pad_byte, entry.size)
348
349            data = self.GetPaddedDataForEntry(entry, entry_data_final)
350            # Handle empty space before the entry
351            pad = (entry.offset or 0) - self._skip_at_start - len(section_data)
352            if pad > 0:
353                section_data += tools.get_bytes(self._pad_byte, pad)
354
355            # Add in the actual entry data
356            if entry.overlap:
357                end_offset = entry.offset + entry.size
358                if end_offset > len(section_data):
359                    entry.Raise("Offset %#x (%d) ending at %#x (%d) must overlap with existing entries" %
360                                (entry.offset, entry.offset, end_offset,
361                                 end_offset))
362                # Don't write anything for null entries'
363                if entry_data is not None:
364                    section_data = (section_data[:entry.offset] + data +
365                                    section_data[entry.offset + entry.size:])
366            else:
367                section_data += data
368
369        self.Detail('GetData: %d entries, total size %#x' %
370                    (len(self._entries), len(section_data)))
371        return self.CompressData(section_data)
372
373    def GetPaddedData(self, data=None):
374        """Get the data for a section including any padding
375
376        Gets the section data and uses the parent section's pad-byte value to
377        add padding before and after as defined by the pad-before and pad-after
378        properties. If this is a top-level section (i.e. an image), this is the
379        same as GetData(), since padding is not supported.
380
381        This does not consider alignment.
382
383        Returns:
384            Contents of the section along with any pad bytes before and
385            after it (bytes)
386        """
387        section = self.section or self
388        if data is None:
389            data = self.GetData()
390        return section.GetPaddedDataForEntry(self, data)
391
392    def GetData(self, required=True):
393        """Get the contents of an entry
394
395        This builds the contents of the section, stores this as the contents of
396        the section and returns it. If the section has a filename, the data is
397        written there also.
398
399        Args:
400            required: True if the data must be present, False if it is OK to
401                return None
402
403        Returns:
404            bytes content of the section, made up for all all of its subentries.
405            This excludes any padding. If the section is compressed, the
406            compressed data is returned
407        """
408        if not self.build_done:
409            data = self.BuildSectionData(required)
410            if data is None:
411                return None
412            self.SetContents(data)
413        else:
414            data = self.data
415        if self._filename:
416            tools.write_file(tools.get_output_filename(self._filename), data)
417        return data
418
419    def GetOffsets(self):
420        """Handle entries that want to set the offset/size of other entries
421
422        This calls each entry's GetOffsets() method. If it returns a list
423        of entries to update, it updates them.
424        """
425        self.GetEntryOffsets()
426        return {}
427
428    def ResetForPack(self):
429        """Reset offset/size fields so that packing can be done again"""
430        super().ResetForPack()
431        for entry in self._entries.values():
432            entry.ResetForPack()
433
434    def Pack(self, offset):
435        """Pack all entries into the section"""
436        self._PackEntries()
437        if self._sort:
438            self._SortEntries()
439        self._extend_entries()
440
441        if self.build_done:
442            self.size = None
443        else:
444            data = self.BuildSectionData(True)
445            self.SetContents(data)
446
447        self.CheckSize()
448
449        offset = super().Pack(offset)
450        self.CheckEntries()
451        return offset
452
453    def _PackEntries(self):
454        """Pack all entries into the section"""
455        offset = self._skip_at_start
456        for entry in self._entries.values():
457            offset = entry.Pack(offset)
458        return offset
459
460    def _extend_entries(self):
461        """Extend any entries that are permitted to"""
462        exp_entry = None
463        for entry in self._entries.values():
464            if exp_entry:
465                exp_entry.extend_to_limit(entry.offset)
466                exp_entry = None
467            if entry.extend_size:
468                exp_entry = entry
469        if exp_entry:
470            exp_entry.extend_to_limit(self.size)
471
472    def _SortEntries(self):
473        """Sort entries by offset"""
474        entries = sorted(self._entries.values(), key=lambda entry: entry.offset)
475        self._entries.clear()
476        for entry in entries:
477            self._entries[entry._node.name] = entry
478
479    def CheckEntries(self):
480        """Check that entries do not overlap or extend outside the section"""
481        max_size = self.size if self.uncomp_size is None else self.uncomp_size
482
483        offset = 0
484        prev_name = 'None'
485        for entry in self._entries.values():
486            entry.CheckEntries()
487            if (entry.offset < self._skip_at_start or
488                    entry.offset + entry.size > self._skip_at_start +
489                    max_size):
490                entry.Raise('Offset %#x (%d) size %#x (%d) is outside the '
491                            "section '%s' starting at %#x (%d) "
492                            'of size %#x (%d)' %
493                            (entry.offset, entry.offset, entry.size, entry.size,
494                             self._node.path, self._skip_at_start,
495                             self._skip_at_start, max_size, max_size))
496            if not entry.overlap:
497                if entry.offset < offset and entry.size:
498                    entry.Raise("Offset %#x (%d) overlaps with previous entry '%s' ending at %#x (%d)" %
499                                (entry.offset, entry.offset, prev_name, offset,
500                                 offset))
501                offset = entry.offset + entry.size
502                prev_name = entry.GetPath()
503
504    def WriteSymbols(self, section):
505        """Write symbol values into binary files for access at run time"""
506        for entry in self._entries.values():
507            entry.WriteSymbols(self)
508
509    def SetCalculatedProperties(self):
510        super().SetCalculatedProperties()
511        if self._end_at_4gb:
512            state.SetInt(self._node, 'skip-at-start', self._skip_at_start)
513        for entry in self._entries.values():
514            entry.SetCalculatedProperties()
515
516    def SetImagePos(self, image_pos):
517        super().SetImagePos(image_pos)
518        if self.compress == 'none':
519            for entry in self._entries.values():
520                entry.SetImagePos(image_pos + self.offset)
521
522    def ProcessContents(self):
523        sizes_ok_base = super(Entry_section, self).ProcessContents()
524        sizes_ok = True
525        for entry in self._entries.values():
526            if not entry.ProcessContents():
527                sizes_ok = False
528        return sizes_ok and sizes_ok_base
529
530    def WriteMap(self, fd, indent):
531        """Write a map of the section to a .map file
532
533        Args:
534            fd: File to write the map to
535        """
536        Entry.WriteMapLine(fd, indent, self.name, self.offset or 0,
537                           self.size, self.image_pos)
538        for entry in self._entries.values():
539            entry.WriteMap(fd, indent + 1)
540
541    def GetEntries(self) -> dict[str, Entry]:
542        return self._entries
543
544    def GetContentsByPhandle(self, phandle, source_entry, required):
545        """Get the data contents of an entry specified by a phandle
546
547        This uses a phandle to look up a node and and find the entry
548        associated with it. Then it returns the contents of that entry.
549
550        The node must be a direct subnode of this section.
551
552        Args:
553            phandle: Phandle to look up (integer)
554            source_entry: Entry containing that phandle (used for error
555                reporting)
556            required: True if the data must be present, False if it is OK to
557                return None
558
559        Returns:
560            data from associated entry (as a string), or None if not found
561        """
562        node = self._node.GetFdt().LookupPhandle(phandle)
563        if not node:
564            source_entry.Raise("Cannot find node for phandle %d" % phandle)
565        entry = self.FindEntryByNode(node)
566        if not entry:
567            source_entry.Raise("Cannot find entry for node '%s'" % node.name)
568        return entry.GetData(required)
569
570    def LookupEntry(self, entries, sym_name, msg):
571        """Look up the entry for a binman symbol
572
573        Args:
574            entries (dict): entries to search:
575                key: entry name
576                value: Entry object
577            sym_name: Symbol name to look up in the format
578                _binman_<entry>_prop_<property> where <entry> is the name of
579                the entry and <property> is the property to find (e.g.
580                _binman_u_boot_prop_offset). As a special case, you can append
581                _any to <entry> to have it search for any matching entry. E.g.
582                _binman_u_boot_any_prop_offset will match entries called u-boot,
583                u-boot-img and u-boot-nodtb)
584            msg: Message to display if an error occurs
585
586        Returns:
587            tuple:
588                Entry: entry object that was found
589                str: name used to search for entries (uses '-' instead of the
590                    '_' used by the symbol name)
591                str: property name the symbol refers to, e.g. 'image_pos'
592
593        Raises:
594            ValueError:the symbol name cannot be decoded, e.g. does not have
595                a '_binman_' prefix
596        """
597        m = re.match(r'^_binman_(\w+)_prop_(\w+)$', sym_name)
598        if not m:
599            raise ValueError("%s: Symbol '%s' has invalid format" %
600                             (msg, sym_name))
601        entry_name, prop_name = m.groups()
602        entry_name = entry_name.replace('_', '-')
603        entry = entries.get(entry_name)
604        if not entry:
605            if entry_name.endswith('-any'):
606                root = entry_name[:-4]
607                for name in entries:
608                    if name.startswith(root):
609                        rest = name[len(root):]
610                        if rest in ['', '-elf', '-img', '-nodtb']:
611                            entry = entries[name]
612        return entry, entry_name, prop_name
613
614    def GetSymbolValue(self, sym_name, optional, msg, base_addr, entries=None):
615        """Get the value of a Binman symbol
616
617        Look up a Binman symbol and obtain its value.
618
619        At present the only entry properties supported are:
620            offset
621            image_pos - 'base_addr' is added if this is not an end-at-4gb image
622            size
623
624        Args:
625            sym_name: Symbol name to look up in the format
626                _binman_<entry>_prop_<property> where <entry> is the name of
627                the entry and <property> is the property to find (e.g.
628                _binman_u_boot_prop_offset). As a special case, you can append
629                _any to <entry> to have it search for any matching entry. E.g.
630                _binman_u_boot_any_prop_offset will match entries called u-boot,
631                u-boot-img and u-boot-nodtb)
632            optional: True if the symbol is optional. If False this function
633                will raise if the symbol is not found
634            msg: Message to display if an error occurs
635            base_addr (int): Base address of image. This is added to the
636                returned value of image-pos so that the returned position
637                indicates where the targeted entry/binary has actually been
638                loaded
639
640        Returns:
641            Value that should be assigned to that symbol, or None if it was
642                optional and not found
643
644        Raises:
645            ValueError if the symbol is invalid or not found, or references a
646                property which is not supported
647        """
648        if not entries:
649            entries = self._entries
650        entry, entry_name, prop_name = self.LookupEntry(entries, sym_name, msg)
651        if not entry:
652            err = ("%s: Entry '%s' not found in list (%s)" %
653                   (msg, entry_name, ','.join(entries.keys())))
654            if optional:
655                print('Warning: %s' % err, file=sys.stderr)
656                return None
657            raise ValueError(err)
658        if prop_name == 'offset':
659            return entry.offset
660        elif prop_name == 'image_pos':
661            if not entry.image_pos:
662                tout.info(f'Symbol-writing: no value for {entry._node.path}')
663                return None
664            return base_addr + entry.image_pos
665        if prop_name == 'size':
666            return entry.size
667        else:
668            raise ValueError("%s: No such property '%s'" % (msg, prop_name))
669
670    def GetStartOffset(self):
671        """Get the start offset for this section
672
673        Returns:
674            The first available offset in this section (typically 0)
675        """
676        return self._skip_at_start
677
678    def GetImageSize(self):
679        """Get the size of the image containing this section
680
681        Returns:
682            Image size as an integer number of bytes, which may be None if the
683                image size is dynamic and its sections have not yet been packed
684        """
685        return self.GetImage().size
686
687    def FindEntryType(self, etype):
688        """Find an entry type in the section
689
690        Args:
691            etype: Entry type to find
692        Returns:
693            entry matching that type, or None if not found
694        """
695        for entry in self._entries.values():
696            if entry.etype == etype:
697                return entry
698        return None
699
700    def GetEntryContents(self, skip_entry=None):
701        """Call ObtainContents() for each entry in the section
702
703        The overall goal of this function is to read in any available data in
704        this entry and any subentries. This includes reading in blobs, setting
705        up objects which have predefined contents, etc.
706
707        Since entry types which contain entries call ObtainContents() on all
708        those entries too, the result is that ObtainContents() is called
709        recursively for the whole tree below this one.
710
711        Entries with subentries are generally not *themselves& processed here,
712        i.e. their ObtainContents() implementation simply obtains contents of
713        their subentries, skipping their own contents. For example, the
714        implementation here (for entry_Section) does not attempt to pack the
715        entries into a final result. That is handled later.
716
717        Generally, calling this results in SetContents() being called for each
718        entry, so that the 'data' and 'contents_size; properties are set, and
719        subsequent calls to GetData() will return value data.
720
721        Where 'allow_missing' is set, this can result in the 'missing' property
722        being set to True if there is no data. This is handled by setting the
723        data to b''. This function will still return success. Future calls to
724        GetData() for this entry will return b'', or in the case where the data
725        is faked, GetData() will return that fake data.
726
727        Args:
728            skip_entry: (single) Entry to skip, or None to process all entries
729
730        Note that this may set entry.absent to True if the entry is not
731        actually needed
732        """
733        def _CheckDone(entry):
734            if entry != skip_entry:
735                if entry.ObtainContents() is False:
736                    next_todo.append(entry)
737            return entry
738
739        todo = self.GetEntries().values()
740        for passnum in range(3):
741            threads = state.GetThreads()
742            next_todo = []
743
744            if threads == 0:
745                for entry in todo:
746                    _CheckDone(entry)
747            else:
748                with concurrent.futures.ThreadPoolExecutor(
749                        max_workers=threads) as executor:
750                    future_to_data = {
751                        entry: executor.submit(_CheckDone, entry)
752                        for entry in todo}
753                    timeout = 60
754                    if self.GetImage().test_section_timeout:
755                        timeout = 0
756                    done, not_done = concurrent.futures.wait(
757                        future_to_data.values(), timeout=timeout)
758                    # Make sure we check the result, so any exceptions are
759                    # generated. Check the results in entry order, since tests
760                    # may expect earlier entries to fail first.
761                    for entry in todo:
762                        job = future_to_data[entry]
763                        job.result()
764                    if not_done:
765                        self.Raise('Timed out obtaining contents')
766
767            todo = next_todo
768            if not todo:
769                break
770
771        if todo:
772            self.Raise('Internal error: Could not complete processing of contents: remaining %s' %
773                       todo)
774        return True
775
776    def drop_absent_optional(self) -> None:
777        """Drop entries which are absent.
778        Call for all nodes in the tree. Leaf nodes will do nothing per
779        definition. Sections however have _entries and should drop all children
780        which are absent.
781        """
782        self._entries = {n: e for n, e in self._entries.items() if not (e.absent and e.optional)}
783        # Drop nodes first before traversing children to avoid superfluous calls
784        # to children of absent nodes.
785        for e in self.GetEntries().values():
786            e.drop_absent_optional()
787
788    def _SetEntryOffsetSize(self, name, offset, size):
789        """Set the offset and size of an entry
790
791        Args:
792            name: Entry name to update
793            offset: New offset, or None to leave alone
794            size: New size, or None to leave alone
795        """
796        entry = self._entries.get(name)
797        if not entry:
798            self._Raise("Unable to set offset/size for unknown entry '%s'" %
799                        name)
800        entry.SetOffsetSize(offset + self._skip_at_start if offset is not None
801                            else None, size)
802
803    def GetEntryOffsets(self):
804        """Handle entries that want to set the offset/size of other entries
805
806        This calls each entry's GetOffsets() method. If it returns a list
807        of entries to update, it updates them.
808        """
809        for entry in self._entries.values():
810            offset_dict = entry.GetOffsets()
811            for name, info in offset_dict.items():
812                self._SetEntryOffsetSize(name, *info)
813
814    def CheckSize(self):
815        contents_size = len(self.data)
816
817        size = self.size
818        if not size:
819            data = self.GetPaddedData(self.data)
820            size = len(data)
821            size = tools.align(size, self.align_size)
822
823        if self.size and contents_size > self.size:
824            self._Raise("contents size %#x (%d) exceeds section size %#x (%d)" %
825                        (contents_size, contents_size, self.size, self.size))
826        if not self.size:
827            self.size = size
828        if self.size != tools.align(self.size, self.align_size):
829            self._Raise("Size %#x (%d) does not match align-size %#x (%d)" %
830                        (self.size, self.size, self.align_size,
831                         self.align_size))
832        return size
833
834    def ListEntries(self, entries, indent):
835        """List the files in the section"""
836        Entry.AddEntryInfo(entries, indent, self.name, self.etype, self.size,
837                           self.image_pos, None, self.offset, self)
838        for entry in self._entries.values():
839            entry.ListEntries(entries, indent + 1)
840
841    def LoadData(self, decomp=True):
842        for entry in self._entries.values():
843            entry.LoadData(decomp)
844        data = self.ReadData(decomp)
845        self.contents_size = len(data)
846        self.ProcessContentsUpdate(data)
847        self.Detail('Loaded data')
848
849    def GetImage(self):
850        """Get the image containing this section
851
852        Note that a top-level section is actually an Image, so this function may
853        return self.
854
855        Returns:
856            Image object containing this section
857        """
858        if not self.section:
859            return self
860        return self.section.GetImage()
861
862    def GetSort(self):
863        """Check if the entries in this section will be sorted
864
865        Returns:
866            True if to be sorted, False if entries will be left in the order
867                they appear in the device tree
868        """
869        return self._sort
870
871    def ReadData(self, decomp=True, alt_format=None):
872        tout.info("ReadData path='%s'" % self.GetPath())
873        parent_data = self.section.ReadData(True, alt_format)
874        offset = self.offset - self.section._skip_at_start
875        data = parent_data[offset:offset + self.size]
876        tout.info(
877            '%s: Reading data from offset %#x-%#x (real %#x), size %#x, got %#x' %
878                  (self.GetPath(), self.offset, self.offset + self.size, offset,
879                   self.size, len(data)))
880        return data
881
882    def ReadChildData(self, child, decomp=True, alt_format=None):
883        tout.debug(f"ReadChildData for child '{child.GetPath()}'")
884        parent_data = self.ReadData(True, alt_format)
885        offset = child.offset - self._skip_at_start
886        tout.debug("Extract for child '%s': offset %#x, skip_at_start %#x, result %#x" %
887                   (child.GetPath(), child.offset, self._skip_at_start, offset))
888        data = parent_data[offset:offset + child.size]
889        if decomp:
890            indata = data
891            data = child.DecompressData(indata)
892            if child.uncomp_size:
893                tout.info("%s: Decompressing data size %#x with algo '%s' to data size %#x" %
894                            (child.GetPath(), len(indata), child.compress,
895                            len(data)))
896        if alt_format:
897            new_data = child.GetAltFormat(data, alt_format)
898            if new_data is not None:
899                data = new_data
900        return data
901
902    def WriteData(self, data, decomp=True):
903        ok = super().WriteData(data, decomp)
904
905        # The section contents are now fixed and cannot be rebuilt from the
906        # containing entries.
907        self.mark_build_done()
908        return ok
909
910    def WriteChildData(self, child):
911        return super().WriteChildData(child)
912
913    def SetAllowMissing(self, allow_missing):
914        """Set whether a section allows missing external blobs
915
916        Args:
917            allow_missing: True if allowed, False if not allowed
918        """
919        self.allow_missing = allow_missing
920        for entry in self.GetEntries().values():
921            entry.SetAllowMissing(allow_missing)
922
923    def SetAllowFakeBlob(self, allow_fake):
924        """Set whether a section allows to create a fake blob
925
926        Args:
927            allow_fake: True if allowed, False if not allowed
928        """
929        super().SetAllowFakeBlob(allow_fake)
930        for entry in self.GetEntries().values():
931            entry.SetAllowFakeBlob(allow_fake)
932
933    def CheckMissing(self, missing_list):
934        """Check if any entries in this section have missing external blobs
935
936        If there are missing (non-optional) blobs, the entries are added to the
937        list
938
939        Args:
940            missing_list: List of Entry objects to be added to
941        """
942        for entry in self.GetEntries().values():
943            entry.CheckMissing(missing_list)
944
945    def CheckFakedBlobs(self, faked_blobs_list):
946        """Check if any entries in this section have faked external blobs
947
948        If there are faked blobs, the entries are added to the list
949
950        Args:
951            faked_blobs_list: List of Entry objects to be added to
952        """
953        for entry in self.GetEntries().values():
954            entry.CheckFakedBlobs(faked_blobs_list)
955
956    def CheckOptional(self, optional_list):
957        """Check the section for missing but optional external blobs
958
959        If there are missing (optional) blobs, the entries are added to the list
960
961        Args:
962            optional_list (list): List of Entry objects to be added to
963        """
964        for entry in self.GetEntries().values():
965            entry.CheckOptional(optional_list)
966
967    def check_missing_bintools(self, missing_list):
968        """Check if any entries in this section have missing bintools
969
970        If there are missing bintools, these are added to the list
971
972        Args:
973            missing_list: List of Bintool objects to be added to
974        """
975        super().check_missing_bintools(missing_list)
976        for entry in self.GetEntries().values():
977            entry.check_missing_bintools(missing_list)
978
979    def _CollectEntries(self, entries, entries_by_name, add_entry):
980        """Collect all the entries in an section
981
982        This builds up a dict of entries in this section and all subsections.
983        Entries are indexed by path and by name.
984
985        Since all paths are unique, entries will not have any conflicts. However
986        entries_by_name make have conflicts if two entries have the same name
987        (e.g. with different parent sections). In this case, an entry at a
988        higher level in the hierarchy will win over a lower-level entry.
989
990        Args:
991            entries: dict to put entries:
992                key: entry path
993                value: Entry object
994            entries_by_name: dict to put entries
995                key: entry name
996                value: Entry object
997            add_entry: Entry to add
998        """
999        entries[add_entry.GetPath()] = add_entry
1000        to_add = add_entry.GetEntries()
1001        if to_add:
1002            for entry in to_add.values():
1003                entries[entry.GetPath()] = entry
1004            for entry in to_add.values():
1005                self._CollectEntries(entries, entries_by_name, entry)
1006        entries_by_name[add_entry.name] = add_entry
1007
1008    def MissingArgs(self, entry, missing):
1009        """Report a missing argument, if enabled
1010
1011        For entries which require arguments, this reports an error if some are
1012        missing. If missing entries are being ignored (e.g. because we read the
1013        entry from an image rather than creating it), this function does
1014        nothing.
1015
1016        Args:
1017            entry (Entry): Entry to raise the error on
1018            missing (list of str): List of missing properties / entry args, each
1019            a string
1020        """
1021        if not self._ignore_missing:
1022            missing = ', '.join(missing)
1023            entry.Raise(f'Missing required properties/entry args: {missing}')
1024
1025    def CheckAltFormats(self, alt_formats):
1026        for entry in self.GetEntries().values():
1027            entry.CheckAltFormats(alt_formats)
1028
1029    def AddBintools(self, btools):
1030        super().AddBintools(btools)
1031        for entry in self.GetEntries().values():
1032            entry.AddBintools(btools)
1033
1034    def read_elf_segments(self):
1035        entries = self.GetEntries()
1036
1037        # If the section only has one entry, see if it can provide ELF segments
1038        if len(entries) == 1:
1039            for entry in entries.values():
1040                return entry.read_elf_segments()
1041        return None
1042