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