162306a36Sopenharmony_ci#!/usr/bin/env python
262306a36Sopenharmony_ci# SPDX-License-Identifier: GPL-2.0
362306a36Sopenharmony_ci# -*- coding: utf-8; mode: python -*-
462306a36Sopenharmony_ci# pylint: disable=R0903, C0330, R0914, R0912, E0401
562306a36Sopenharmony_ci
662306a36Sopenharmony_ciu"""
762306a36Sopenharmony_ci    maintainers-include
862306a36Sopenharmony_ci    ~~~~~~~~~~~~~~~~~~~
962306a36Sopenharmony_ci
1062306a36Sopenharmony_ci    Implementation of the ``maintainers-include`` reST-directive.
1162306a36Sopenharmony_ci
1262306a36Sopenharmony_ci    :copyright:  Copyright (C) 2019  Kees Cook <keescook@chromium.org>
1362306a36Sopenharmony_ci    :license:    GPL Version 2, June 1991 see linux/COPYING for details.
1462306a36Sopenharmony_ci
1562306a36Sopenharmony_ci    The ``maintainers-include`` reST-directive performs extensive parsing
1662306a36Sopenharmony_ci    specific to the Linux kernel's standard "MAINTAINERS" file, in an
1762306a36Sopenharmony_ci    effort to avoid needing to heavily mark up the original plain text.
1862306a36Sopenharmony_ci"""
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_ciimport sys
2162306a36Sopenharmony_ciimport re
2262306a36Sopenharmony_ciimport os.path
2362306a36Sopenharmony_ci
2462306a36Sopenharmony_cifrom docutils import statemachine
2562306a36Sopenharmony_cifrom docutils.utils.error_reporting import ErrorString
2662306a36Sopenharmony_cifrom docutils.parsers.rst import Directive
2762306a36Sopenharmony_cifrom docutils.parsers.rst.directives.misc import Include
2862306a36Sopenharmony_ci
2962306a36Sopenharmony_ci__version__  = '1.0'
3062306a36Sopenharmony_ci
3162306a36Sopenharmony_cidef setup(app):
3262306a36Sopenharmony_ci    app.add_directive("maintainers-include", MaintainersInclude)
3362306a36Sopenharmony_ci    return dict(
3462306a36Sopenharmony_ci        version = __version__,
3562306a36Sopenharmony_ci        parallel_read_safe = True,
3662306a36Sopenharmony_ci        parallel_write_safe = True
3762306a36Sopenharmony_ci    )
3862306a36Sopenharmony_ci
3962306a36Sopenharmony_ciclass MaintainersInclude(Include):
4062306a36Sopenharmony_ci    u"""MaintainersInclude (``maintainers-include``) directive"""
4162306a36Sopenharmony_ci    required_arguments = 0
4262306a36Sopenharmony_ci
4362306a36Sopenharmony_ci    def parse_maintainers(self, path):
4462306a36Sopenharmony_ci        """Parse all the MAINTAINERS lines into ReST for human-readability"""
4562306a36Sopenharmony_ci
4662306a36Sopenharmony_ci        result = list()
4762306a36Sopenharmony_ci        result.append(".. _maintainers:")
4862306a36Sopenharmony_ci        result.append("")
4962306a36Sopenharmony_ci
5062306a36Sopenharmony_ci        # Poor man's state machine.
5162306a36Sopenharmony_ci        descriptions = False
5262306a36Sopenharmony_ci        maintainers = False
5362306a36Sopenharmony_ci        subsystems = False
5462306a36Sopenharmony_ci
5562306a36Sopenharmony_ci        # Field letter to field name mapping.
5662306a36Sopenharmony_ci        field_letter = None
5762306a36Sopenharmony_ci        fields = dict()
5862306a36Sopenharmony_ci
5962306a36Sopenharmony_ci        prev = None
6062306a36Sopenharmony_ci        field_prev = ""
6162306a36Sopenharmony_ci        field_content = ""
6262306a36Sopenharmony_ci
6362306a36Sopenharmony_ci        for line in open(path):
6462306a36Sopenharmony_ci            # Have we reached the end of the preformatted Descriptions text?
6562306a36Sopenharmony_ci            if descriptions and line.startswith('Maintainers'):
6662306a36Sopenharmony_ci                descriptions = False
6762306a36Sopenharmony_ci                # Ensure a blank line following the last "|"-prefixed line.
6862306a36Sopenharmony_ci                result.append("")
6962306a36Sopenharmony_ci
7062306a36Sopenharmony_ci            # Start subsystem processing? This is to skip processing the text
7162306a36Sopenharmony_ci            # between the Maintainers heading and the first subsystem name.
7262306a36Sopenharmony_ci            if maintainers and not subsystems:
7362306a36Sopenharmony_ci                if re.search('^[A-Z0-9]', line):
7462306a36Sopenharmony_ci                    subsystems = True
7562306a36Sopenharmony_ci
7662306a36Sopenharmony_ci            # Drop needless input whitespace.
7762306a36Sopenharmony_ci            line = line.rstrip()
7862306a36Sopenharmony_ci
7962306a36Sopenharmony_ci            # Linkify all non-wildcard refs to ReST files in Documentation/.
8062306a36Sopenharmony_ci            pat = r'(Documentation/([^\s\?\*]*)\.rst)'
8162306a36Sopenharmony_ci            m = re.search(pat, line)
8262306a36Sopenharmony_ci            if m:
8362306a36Sopenharmony_ci                # maintainers.rst is in a subdirectory, so include "../".
8462306a36Sopenharmony_ci                line = re.sub(pat, ':doc:`%s <../%s>`' % (m.group(2), m.group(2)), line)
8562306a36Sopenharmony_ci
8662306a36Sopenharmony_ci            # Check state machine for output rendering behavior.
8762306a36Sopenharmony_ci            output = None
8862306a36Sopenharmony_ci            if descriptions:
8962306a36Sopenharmony_ci                # Escape the escapes in preformatted text.
9062306a36Sopenharmony_ci                output = "| %s" % (line.replace("\\", "\\\\"))
9162306a36Sopenharmony_ci                # Look for and record field letter to field name mappings:
9262306a36Sopenharmony_ci                #   R: Designated *reviewer*: FullName <address@domain>
9362306a36Sopenharmony_ci                m = re.search(r"\s(\S):\s", line)
9462306a36Sopenharmony_ci                if m:
9562306a36Sopenharmony_ci                    field_letter = m.group(1)
9662306a36Sopenharmony_ci                if field_letter and not field_letter in fields:
9762306a36Sopenharmony_ci                    m = re.search(r"\*([^\*]+)\*", line)
9862306a36Sopenharmony_ci                    if m:
9962306a36Sopenharmony_ci                        fields[field_letter] = m.group(1)
10062306a36Sopenharmony_ci            elif subsystems:
10162306a36Sopenharmony_ci                # Skip empty lines: subsystem parser adds them as needed.
10262306a36Sopenharmony_ci                if len(line) == 0:
10362306a36Sopenharmony_ci                    continue
10462306a36Sopenharmony_ci                # Subsystem fields are batched into "field_content"
10562306a36Sopenharmony_ci                if line[1] != ':':
10662306a36Sopenharmony_ci                    # Render a subsystem entry as:
10762306a36Sopenharmony_ci                    #   SUBSYSTEM NAME
10862306a36Sopenharmony_ci                    #   ~~~~~~~~~~~~~~
10962306a36Sopenharmony_ci
11062306a36Sopenharmony_ci                    # Flush pending field content.
11162306a36Sopenharmony_ci                    output = field_content + "\n\n"
11262306a36Sopenharmony_ci                    field_content = ""
11362306a36Sopenharmony_ci
11462306a36Sopenharmony_ci                    # Collapse whitespace in subsystem name.
11562306a36Sopenharmony_ci                    heading = re.sub(r"\s+", " ", line)
11662306a36Sopenharmony_ci                    output = output + "%s\n%s" % (heading, "~" * len(heading))
11762306a36Sopenharmony_ci                    field_prev = ""
11862306a36Sopenharmony_ci                else:
11962306a36Sopenharmony_ci                    # Render a subsystem field as:
12062306a36Sopenharmony_ci                    #   :Field: entry
12162306a36Sopenharmony_ci                    #           entry...
12262306a36Sopenharmony_ci                    field, details = line.split(':', 1)
12362306a36Sopenharmony_ci                    details = details.strip()
12462306a36Sopenharmony_ci
12562306a36Sopenharmony_ci                    # Mark paths (and regexes) as literal text for improved
12662306a36Sopenharmony_ci                    # readability and to escape any escapes.
12762306a36Sopenharmony_ci                    if field in ['F', 'N', 'X', 'K']:
12862306a36Sopenharmony_ci                        # But only if not already marked :)
12962306a36Sopenharmony_ci                        if not ':doc:' in details:
13062306a36Sopenharmony_ci                            details = '``%s``' % (details)
13162306a36Sopenharmony_ci
13262306a36Sopenharmony_ci                    # Comma separate email field continuations.
13362306a36Sopenharmony_ci                    if field == field_prev and field_prev in ['M', 'R', 'L']:
13462306a36Sopenharmony_ci                        field_content = field_content + ","
13562306a36Sopenharmony_ci
13662306a36Sopenharmony_ci                    # Do not repeat field names, so that field entries
13762306a36Sopenharmony_ci                    # will be collapsed together.
13862306a36Sopenharmony_ci                    if field != field_prev:
13962306a36Sopenharmony_ci                        output = field_content + "\n"
14062306a36Sopenharmony_ci                        field_content = ":%s:" % (fields.get(field, field))
14162306a36Sopenharmony_ci                    field_content = field_content + "\n\t%s" % (details)
14262306a36Sopenharmony_ci                    field_prev = field
14362306a36Sopenharmony_ci            else:
14462306a36Sopenharmony_ci                output = line
14562306a36Sopenharmony_ci
14662306a36Sopenharmony_ci            # Re-split on any added newlines in any above parsing.
14762306a36Sopenharmony_ci            if output != None:
14862306a36Sopenharmony_ci                for separated in output.split('\n'):
14962306a36Sopenharmony_ci                    result.append(separated)
15062306a36Sopenharmony_ci
15162306a36Sopenharmony_ci            # Update the state machine when we find heading separators.
15262306a36Sopenharmony_ci            if line.startswith('----------'):
15362306a36Sopenharmony_ci                if prev.startswith('Descriptions'):
15462306a36Sopenharmony_ci                    descriptions = True
15562306a36Sopenharmony_ci                if prev.startswith('Maintainers'):
15662306a36Sopenharmony_ci                    maintainers = True
15762306a36Sopenharmony_ci
15862306a36Sopenharmony_ci            # Retain previous line for state machine transitions.
15962306a36Sopenharmony_ci            prev = line
16062306a36Sopenharmony_ci
16162306a36Sopenharmony_ci        # Flush pending field contents.
16262306a36Sopenharmony_ci        if field_content != "":
16362306a36Sopenharmony_ci            for separated in field_content.split('\n'):
16462306a36Sopenharmony_ci                result.append(separated)
16562306a36Sopenharmony_ci
16662306a36Sopenharmony_ci        output = "\n".join(result)
16762306a36Sopenharmony_ci        # For debugging the pre-rendered results...
16862306a36Sopenharmony_ci        #print(output, file=open("/tmp/MAINTAINERS.rst", "w"))
16962306a36Sopenharmony_ci
17062306a36Sopenharmony_ci        self.state_machine.insert_input(
17162306a36Sopenharmony_ci          statemachine.string2lines(output), path)
17262306a36Sopenharmony_ci
17362306a36Sopenharmony_ci    def run(self):
17462306a36Sopenharmony_ci        """Include the MAINTAINERS file as part of this reST file."""
17562306a36Sopenharmony_ci        if not self.state.document.settings.file_insertion_enabled:
17662306a36Sopenharmony_ci            raise self.warning('"%s" directive disabled.' % self.name)
17762306a36Sopenharmony_ci
17862306a36Sopenharmony_ci        # Walk up source path directories to find Documentation/../
17962306a36Sopenharmony_ci        path = self.state_machine.document.attributes['source']
18062306a36Sopenharmony_ci        path = os.path.realpath(path)
18162306a36Sopenharmony_ci        tail = path
18262306a36Sopenharmony_ci        while tail != "Documentation" and tail != "":
18362306a36Sopenharmony_ci            (path, tail) = os.path.split(path)
18462306a36Sopenharmony_ci
18562306a36Sopenharmony_ci        # Append "MAINTAINERS"
18662306a36Sopenharmony_ci        path = os.path.join(path, "MAINTAINERS")
18762306a36Sopenharmony_ci
18862306a36Sopenharmony_ci        try:
18962306a36Sopenharmony_ci            self.state.document.settings.record_dependencies.add(path)
19062306a36Sopenharmony_ci            lines = self.parse_maintainers(path)
19162306a36Sopenharmony_ci        except IOError as error:
19262306a36Sopenharmony_ci            raise self.severe('Problems with "%s" directive path:\n%s.' %
19362306a36Sopenharmony_ci                      (self.name, ErrorString(error)))
19462306a36Sopenharmony_ci
19562306a36Sopenharmony_ci        return []
196