summaryrefslogtreecommitdiffstats
path: root/dlopen-notes.py
diff options
context:
space:
mode:
Diffstat (limited to 'dlopen-notes.py')
-rwxr-xr-xdlopen-notes.py165
1 files changed, 165 insertions, 0 deletions
diff --git a/dlopen-notes.py b/dlopen-notes.py
new file mode 100755
index 0000000..29ea270
--- /dev/null
+++ b/dlopen-notes.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: CC0-1.0
+
+"""\
+Read .note.dlopen notes from ELF files and report the contents
+"""
+
+import argparse
+import enum
+import functools
+import json
+import sys
+from elftools.elf.elffile import ELFFile
+from elftools.elf.sections import NoteSection
+
+try:
+ import rich
+ print_json = rich.print_json
+except ImportError:
+ print_json = print
+
+def listify(f):
+ def wrap(*args, **kwargs):
+ return list(f(*args, **kwargs))
+ return functools.update_wrapper(wrap, f)
+
+@listify
+def read_dlopen_notes(filename):
+ elffile = ELFFile(open(filename, 'rb'))
+
+ for section in elffile.iter_sections():
+ if not isinstance(section, NoteSection) or section.name != '.note.dlopen':
+ continue
+
+ for note in section.iter_notes():
+ if note['n_type'] != 0x407c0c0a or note['n_name'] != 'FDO':
+ continue
+ note_desc = note['n_desc']
+
+ try:
+ # On older Python versions (e.g.: Ubuntu 22.04) we get a string, on
+ # newer versions a bytestring
+ if not isinstance(note_desc, str):
+ text = note_desc.decode('utf-8').rstrip('\0')
+ else:
+ text = note_desc.rstrip('\0')
+ except UnicodeDecodeError as e:
+ raise ValueError(f'{filename}: Invalid UTF-8 in .note.dlopen n_desc') from e
+
+ try:
+ j = json.loads(text)
+ except json.JSONDecodeError as e:
+ raise ValueError(f'{filename}: Invalid JSON in .note.dlopen note_desc') from e
+
+ if not isinstance(j, list):
+ print(f'{filename}: ignoring .note.dlopen n_desc with JSON that is not a list',
+ file=sys.stderr)
+ continue
+
+ yield from j
+
+def dictify(f):
+ def wrap(*args, **kwargs):
+ return dict(f(*args, **kwargs))
+ return functools.update_wrapper(wrap, f)
+
+@dictify
+def group_by_soname(notes):
+ for note in notes:
+ for element in note:
+ priority = element.get('priority', 'recommended')
+ for soname in element['soname']:
+ yield soname, priority
+
+class Priority(enum.Enum):
+ suggested = 1
+ recommended = 2
+ required = 3
+
+ def __lt__(self, other):
+ return self.value < other.value
+
+def group_by_feature(filenames, notes):
+ features = {}
+
+ # We expect each note to be in the format:
+ # [
+ # {
+ # "feature": "...",
+ # "description": "...",
+ # "priority": "required"|"recommended"|"suggested",
+ # "soname": ["..."],
+ # }
+ # ]
+ for filename, note_group in zip(filenames, notes):
+ for note in note_group:
+ prio = Priority[note.get('priority', 'recommened')]
+ feature_name = note['feature']
+
+ try:
+ feature = features[feature_name]
+ except KeyError:
+ # Create new
+ feature = features[feature_name] = {
+ 'description': note.get('description', ''),
+ 'sonames': { soname:prio for soname in note['soname'] },
+ }
+ else:
+ # Merge
+ if feature['description'] != note.get('description', ''):
+ print(f"{filename}: feature {note['feature']!r} found with different description, ignoring",
+ file=sys.stderr)
+
+ for soname in note['soname']:
+ highest = max(feature['sonames'].get(soname, Priority.suggested),
+ prio)
+ feature['sonames'][soname] = highest
+
+ return features
+
+def parse_args():
+ p = argparse.ArgumentParser(description=__doc__)
+ p.add_argument('--raw',
+ action='store_true',
+ help='show the original JSON extracted from input files')
+ p.add_argument('--sonames',
+ action='store_true',
+ help='list all sonames and their priorities, one soname per line')
+ p.add_argument('--features',
+ nargs='?',
+ const=[],
+ type=lambda s: s.split(','),
+ action='extend',
+ metavar='FEATURE1,FEATURE2',
+ help='describe features, can be specified multiple times')
+ p.add_argument('filenames', nargs='+', metavar='filename')
+ return p.parse_args()
+
+if __name__ == '__main__':
+ args = parse_args()
+
+ notes = [read_dlopen_notes(filename) for filename in args.filenames]
+
+ if args.raw:
+ for filename, note in zip(args.filenames, notes):
+ print(f'# {filename}')
+ print_json(json.dumps(note, indent=2))
+
+ if args.features is not None:
+ features = group_by_feature(args.filenames, notes)
+
+ toprint = {name:feature for name,feature in features.items()
+ if name in args.features or not args.features}
+ if len(toprint) < len(args.features):
+ sys.exit('Some features were not found')
+
+ print('# grouped by feature')
+ print_json(json.dumps(toprint,
+ indent=2,
+ default=lambda prio: prio.name))
+
+ if args.sonames:
+ sonames = group_by_soname(notes)
+ for soname in sorted(sonames.keys()):
+ print(f"{soname} {sonames[soname]}")