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