162306a36Sopenharmony_ci#!/usr/bin/env python3
262306a36Sopenharmony_ci# -*- coding: utf-8; mode: python -*-
362306a36Sopenharmony_ci# pylint: disable=R0903, C0330, R0914, R0912, E0401
462306a36Sopenharmony_ci
562306a36Sopenharmony_ciu"""
662306a36Sopenharmony_ci    kernel-include
762306a36Sopenharmony_ci    ~~~~~~~~~~~~~~
862306a36Sopenharmony_ci
962306a36Sopenharmony_ci    Implementation of the ``kernel-include`` reST-directive.
1062306a36Sopenharmony_ci
1162306a36Sopenharmony_ci    :copyright:  Copyright (C) 2016  Markus Heiser
1262306a36Sopenharmony_ci    :license:    GPL Version 2, June 1991 see linux/COPYING for details.
1362306a36Sopenharmony_ci
1462306a36Sopenharmony_ci    The ``kernel-include`` reST-directive is a replacement for the ``include``
1562306a36Sopenharmony_ci    directive. The ``kernel-include`` directive expand environment variables in
1662306a36Sopenharmony_ci    the path name and allows to include files from arbitrary locations.
1762306a36Sopenharmony_ci
1862306a36Sopenharmony_ci    .. hint::
1962306a36Sopenharmony_ci
2062306a36Sopenharmony_ci      Including files from arbitrary locations (e.g. from ``/etc``) is a
2162306a36Sopenharmony_ci      security risk for builders. This is why the ``include`` directive from
2262306a36Sopenharmony_ci      docutils *prohibit* pathnames pointing to locations *above* the filesystem
2362306a36Sopenharmony_ci      tree where the reST document with the include directive is placed.
2462306a36Sopenharmony_ci
2562306a36Sopenharmony_ci    Substrings of the form $name or ${name} are replaced by the value of
2662306a36Sopenharmony_ci    environment variable name. Malformed variable names and references to
2762306a36Sopenharmony_ci    non-existing variables are left unchanged.
2862306a36Sopenharmony_ci"""
2962306a36Sopenharmony_ci
3062306a36Sopenharmony_ci# ==============================================================================
3162306a36Sopenharmony_ci# imports
3262306a36Sopenharmony_ci# ==============================================================================
3362306a36Sopenharmony_ci
3462306a36Sopenharmony_ciimport os.path
3562306a36Sopenharmony_ci
3662306a36Sopenharmony_cifrom docutils import io, nodes, statemachine
3762306a36Sopenharmony_cifrom docutils.utils.error_reporting import SafeString, ErrorString
3862306a36Sopenharmony_cifrom docutils.parsers.rst import directives
3962306a36Sopenharmony_cifrom docutils.parsers.rst.directives.body import CodeBlock, NumberLines
4062306a36Sopenharmony_cifrom docutils.parsers.rst.directives.misc import Include
4162306a36Sopenharmony_ci
4262306a36Sopenharmony_ci__version__  = '1.0'
4362306a36Sopenharmony_ci
4462306a36Sopenharmony_ci# ==============================================================================
4562306a36Sopenharmony_cidef setup(app):
4662306a36Sopenharmony_ci# ==============================================================================
4762306a36Sopenharmony_ci
4862306a36Sopenharmony_ci    app.add_directive("kernel-include", KernelInclude)
4962306a36Sopenharmony_ci    return dict(
5062306a36Sopenharmony_ci        version = __version__,
5162306a36Sopenharmony_ci        parallel_read_safe = True,
5262306a36Sopenharmony_ci        parallel_write_safe = True
5362306a36Sopenharmony_ci    )
5462306a36Sopenharmony_ci
5562306a36Sopenharmony_ci# ==============================================================================
5662306a36Sopenharmony_ciclass KernelInclude(Include):
5762306a36Sopenharmony_ci# ==============================================================================
5862306a36Sopenharmony_ci
5962306a36Sopenharmony_ci    u"""KernelInclude (``kernel-include``) directive"""
6062306a36Sopenharmony_ci
6162306a36Sopenharmony_ci    def run(self):
6262306a36Sopenharmony_ci        env = self.state.document.settings.env
6362306a36Sopenharmony_ci        path = os.path.realpath(
6462306a36Sopenharmony_ci            os.path.expandvars(self.arguments[0]))
6562306a36Sopenharmony_ci
6662306a36Sopenharmony_ci        # to get a bit security back, prohibit /etc:
6762306a36Sopenharmony_ci        if path.startswith(os.sep + "etc"):
6862306a36Sopenharmony_ci            raise self.severe(
6962306a36Sopenharmony_ci                'Problems with "%s" directive, prohibited path: %s'
7062306a36Sopenharmony_ci                % (self.name, path))
7162306a36Sopenharmony_ci
7262306a36Sopenharmony_ci        self.arguments[0] = path
7362306a36Sopenharmony_ci
7462306a36Sopenharmony_ci        env.note_dependency(os.path.abspath(path))
7562306a36Sopenharmony_ci
7662306a36Sopenharmony_ci        #return super(KernelInclude, self).run() # won't work, see HINTs in _run()
7762306a36Sopenharmony_ci        return self._run()
7862306a36Sopenharmony_ci
7962306a36Sopenharmony_ci    def _run(self):
8062306a36Sopenharmony_ci        """Include a file as part of the content of this reST file."""
8162306a36Sopenharmony_ci
8262306a36Sopenharmony_ci        # HINT: I had to copy&paste the whole Include.run method. I'am not happy
8362306a36Sopenharmony_ci        # with this, but due to security reasons, the Include.run method does
8462306a36Sopenharmony_ci        # not allow absolute or relative pathnames pointing to locations *above*
8562306a36Sopenharmony_ci        # the filesystem tree where the reST document is placed.
8662306a36Sopenharmony_ci
8762306a36Sopenharmony_ci        if not self.state.document.settings.file_insertion_enabled:
8862306a36Sopenharmony_ci            raise self.warning('"%s" directive disabled.' % self.name)
8962306a36Sopenharmony_ci        source = self.state_machine.input_lines.source(
9062306a36Sopenharmony_ci            self.lineno - self.state_machine.input_offset - 1)
9162306a36Sopenharmony_ci        source_dir = os.path.dirname(os.path.abspath(source))
9262306a36Sopenharmony_ci        path = directives.path(self.arguments[0])
9362306a36Sopenharmony_ci        if path.startswith('<') and path.endswith('>'):
9462306a36Sopenharmony_ci            path = os.path.join(self.standard_include_path, path[1:-1])
9562306a36Sopenharmony_ci        path = os.path.normpath(os.path.join(source_dir, path))
9662306a36Sopenharmony_ci
9762306a36Sopenharmony_ci        # HINT: this is the only line I had to change / commented out:
9862306a36Sopenharmony_ci        #path = utils.relative_path(None, path)
9962306a36Sopenharmony_ci
10062306a36Sopenharmony_ci        path = nodes.reprunicode(path)
10162306a36Sopenharmony_ci        encoding = self.options.get(
10262306a36Sopenharmony_ci            'encoding', self.state.document.settings.input_encoding)
10362306a36Sopenharmony_ci        e_handler=self.state.document.settings.input_encoding_error_handler
10462306a36Sopenharmony_ci        tab_width = self.options.get(
10562306a36Sopenharmony_ci            'tab-width', self.state.document.settings.tab_width)
10662306a36Sopenharmony_ci        try:
10762306a36Sopenharmony_ci            self.state.document.settings.record_dependencies.add(path)
10862306a36Sopenharmony_ci            include_file = io.FileInput(source_path=path,
10962306a36Sopenharmony_ci                                        encoding=encoding,
11062306a36Sopenharmony_ci                                        error_handler=e_handler)
11162306a36Sopenharmony_ci        except UnicodeEncodeError as error:
11262306a36Sopenharmony_ci            raise self.severe('Problems with "%s" directive path:\n'
11362306a36Sopenharmony_ci                              'Cannot encode input file path "%s" '
11462306a36Sopenharmony_ci                              '(wrong locale?).' %
11562306a36Sopenharmony_ci                              (self.name, SafeString(path)))
11662306a36Sopenharmony_ci        except IOError as error:
11762306a36Sopenharmony_ci            raise self.severe('Problems with "%s" directive path:\n%s.' %
11862306a36Sopenharmony_ci                      (self.name, ErrorString(error)))
11962306a36Sopenharmony_ci        startline = self.options.get('start-line', None)
12062306a36Sopenharmony_ci        endline = self.options.get('end-line', None)
12162306a36Sopenharmony_ci        try:
12262306a36Sopenharmony_ci            if startline or (endline is not None):
12362306a36Sopenharmony_ci                lines = include_file.readlines()
12462306a36Sopenharmony_ci                rawtext = ''.join(lines[startline:endline])
12562306a36Sopenharmony_ci            else:
12662306a36Sopenharmony_ci                rawtext = include_file.read()
12762306a36Sopenharmony_ci        except UnicodeError as error:
12862306a36Sopenharmony_ci            raise self.severe('Problem with "%s" directive:\n%s' %
12962306a36Sopenharmony_ci                              (self.name, ErrorString(error)))
13062306a36Sopenharmony_ci        # start-after/end-before: no restrictions on newlines in match-text,
13162306a36Sopenharmony_ci        # and no restrictions on matching inside lines vs. line boundaries
13262306a36Sopenharmony_ci        after_text = self.options.get('start-after', None)
13362306a36Sopenharmony_ci        if after_text:
13462306a36Sopenharmony_ci            # skip content in rawtext before *and incl.* a matching text
13562306a36Sopenharmony_ci            after_index = rawtext.find(after_text)
13662306a36Sopenharmony_ci            if after_index < 0:
13762306a36Sopenharmony_ci                raise self.severe('Problem with "start-after" option of "%s" '
13862306a36Sopenharmony_ci                                  'directive:\nText not found.' % self.name)
13962306a36Sopenharmony_ci            rawtext = rawtext[after_index + len(after_text):]
14062306a36Sopenharmony_ci        before_text = self.options.get('end-before', None)
14162306a36Sopenharmony_ci        if before_text:
14262306a36Sopenharmony_ci            # skip content in rawtext after *and incl.* a matching text
14362306a36Sopenharmony_ci            before_index = rawtext.find(before_text)
14462306a36Sopenharmony_ci            if before_index < 0:
14562306a36Sopenharmony_ci                raise self.severe('Problem with "end-before" option of "%s" '
14662306a36Sopenharmony_ci                                  'directive:\nText not found.' % self.name)
14762306a36Sopenharmony_ci            rawtext = rawtext[:before_index]
14862306a36Sopenharmony_ci
14962306a36Sopenharmony_ci        include_lines = statemachine.string2lines(rawtext, tab_width,
15062306a36Sopenharmony_ci                                                  convert_whitespace=True)
15162306a36Sopenharmony_ci        if 'literal' in self.options:
15262306a36Sopenharmony_ci            # Convert tabs to spaces, if `tab_width` is positive.
15362306a36Sopenharmony_ci            if tab_width >= 0:
15462306a36Sopenharmony_ci                text = rawtext.expandtabs(tab_width)
15562306a36Sopenharmony_ci            else:
15662306a36Sopenharmony_ci                text = rawtext
15762306a36Sopenharmony_ci            literal_block = nodes.literal_block(rawtext, source=path,
15862306a36Sopenharmony_ci                                    classes=self.options.get('class', []))
15962306a36Sopenharmony_ci            literal_block.line = 1
16062306a36Sopenharmony_ci            self.add_name(literal_block)
16162306a36Sopenharmony_ci            if 'number-lines' in self.options:
16262306a36Sopenharmony_ci                try:
16362306a36Sopenharmony_ci                    startline = int(self.options['number-lines'] or 1)
16462306a36Sopenharmony_ci                except ValueError:
16562306a36Sopenharmony_ci                    raise self.error(':number-lines: with non-integer '
16662306a36Sopenharmony_ci                                     'start value')
16762306a36Sopenharmony_ci                endline = startline + len(include_lines)
16862306a36Sopenharmony_ci                if text.endswith('\n'):
16962306a36Sopenharmony_ci                    text = text[:-1]
17062306a36Sopenharmony_ci                tokens = NumberLines([([], text)], startline, endline)
17162306a36Sopenharmony_ci                for classes, value in tokens:
17262306a36Sopenharmony_ci                    if classes:
17362306a36Sopenharmony_ci                        literal_block += nodes.inline(value, value,
17462306a36Sopenharmony_ci                                                      classes=classes)
17562306a36Sopenharmony_ci                    else:
17662306a36Sopenharmony_ci                        literal_block += nodes.Text(value, value)
17762306a36Sopenharmony_ci            else:
17862306a36Sopenharmony_ci                literal_block += nodes.Text(text, text)
17962306a36Sopenharmony_ci            return [literal_block]
18062306a36Sopenharmony_ci        if 'code' in self.options:
18162306a36Sopenharmony_ci            self.options['source'] = path
18262306a36Sopenharmony_ci            codeblock = CodeBlock(self.name,
18362306a36Sopenharmony_ci                                  [self.options.pop('code')], # arguments
18462306a36Sopenharmony_ci                                  self.options,
18562306a36Sopenharmony_ci                                  include_lines, # content
18662306a36Sopenharmony_ci                                  self.lineno,
18762306a36Sopenharmony_ci                                  self.content_offset,
18862306a36Sopenharmony_ci                                  self.block_text,
18962306a36Sopenharmony_ci                                  self.state,
19062306a36Sopenharmony_ci                                  self.state_machine)
19162306a36Sopenharmony_ci            return codeblock.run()
19262306a36Sopenharmony_ci        self.state_machine.insert_input(include_lines, path)
19362306a36Sopenharmony_ci        return []
194