eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py

changeset 7612
ca1ce1e0fcff
child 7613
382f89c11e27
diff -r d546c4e72f52 -r ca1ce1e0fcff eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/Plugins/CheckerPlugins/CodeStyleChecker/Security/SecurityUtils.py	Mon Jun 08 08:17:14 2020 +0200
@@ -0,0 +1,257 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing utility functions used by the security checks.
+"""
+
+import ast
+import os
+
+
+class InvalidModulePath(Exception):
+    """
+    Class defining an exception for invalid module paths.
+    """
+    pass
+
+
+def getModuleQualnameFromPath(path):
+    """
+    Function to get the module's qualified name by analysis of the
+    path.
+    
+    Resolve the absolute pathname and eliminate symlinks. This could result
+    in an incorrect name if symlinks are used to restructure the python lib
+    directory.
+    
+    Starting from the right-most directory component look for __init__.py
+    in the directory component. If it exists then the directory name is
+    part of the module name. Move left to the subsequent directory
+    components until a directory is found without __init__.py.
+    
+    @param path path of the module to be analyzed
+    @type str
+    @return qualified name of the module
+    @rtype str
+    """
+    (head, tail) = os.path.split(path)
+    if head == '' or tail == '':
+        raise InvalidModulePath('Invalid python file path: "{0}"'
+                                ' Missing path or file name'.format(path))
+    
+    qname = [os.path.splitext(tail)[0]]
+    while head not in ['/', '.', '']:
+        if os.path.isfile(os.path.join(head, '__init__.py')):
+            (head, tail) = os.path.split(head)
+            qname.insert(0, tail)
+        else:
+            break
+
+    qualname = '.'.join(qname)
+    return qualname
+
+
+def namespacePathJoin(namespace, name):
+    """
+    Function to extend a given namespace path.
+    
+    @param namespace namespace to be extended
+    @type str
+    @param name node name to be appended
+    @type str
+    @return extended namespace
+    @rtype str
+    """
+    return "{0}.{1}".format(namespace, name)
+
+
+def namespacePathSplit(path):
+    """
+    Function to split a namespace path into a head and tail.
+    
+    Tail will be the last namespace path component and head will
+    be everything leading up to that in the path. This is similar to
+    os.path.split.
+    
+    @param path namespace path to be split
+    @type str
+    @return tuple containing the namespace path head and tail
+    @rtype tuple of (str, str)
+    """
+    return tuple(path.rsplit('.', 1))
+
+
+def getAttrQualName(node, aliases):
+    """
+    Function to get a the full name for the attribute node.
+
+    This will resolve a pseudo-qualified name for the attribute
+    rooted at node as long as all the deeper nodes are Names or
+    Attributes. This will give you how the code referenced the name but
+    will not tell you what the name actually refers to. If we
+    encounter a node without a static name we punt with an
+    empty string. If this encounters something more complex, such as
+    foo.mylist[0](a,b) we just return empty string.
+    
+    @param node attribute node to be treated
+    @type ast.Attribute
+    @param aliases dictionary of import aliases
+    @type dict
+    @return qualified name of the attribute
+    @rtype str
+    """
+    if isinstance(node, ast.Name):
+        if node.id in aliases:
+            return aliases[node.id]
+        return node.id
+    elif isinstance(node, ast.Attribute):
+        name = "{0}.{1}".format(getAttrQualName(node.value, aliases),
+                                node.attr)
+        if name in aliases:
+            return aliases[name]
+        return name
+    else:
+        return ""
+
+
+def getCallName(node, aliases):
+    """
+    Function to extract the call name from an ast.Call node.
+    
+    @param node node to extract information from
+    @type ast.Call
+    @param aliases dictionary of import aliases
+    @type dict
+    @return name of the ast.Call node
+    @rtype str
+    """
+    if isinstance(node.func, ast.Name):
+        if deepgetattr(node, 'func.id') in aliases:
+            return aliases[deepgetattr(node, 'func.id')]
+        return deepgetattr(node, 'func.id')
+    elif isinstance(node.func, ast.Attribute):
+        return getAttrQualName(node.func, aliases)
+    else:
+        return ""
+
+
+def getQualAttr(node, aliases):
+    """
+    Function to extract the qualified name from an ast.Attribute node.
+    
+    @param node node to extract information from
+    @type ast.Attribute
+    @param aliases dictionary of import aliases
+    @type dict
+    @return qualified attribute name
+    @rtype str
+    """
+    prefix = ""
+    if isinstance(node, ast.Attribute):
+        try:
+            val = deepgetattr(node, 'value.id')
+            if val in aliases:
+                prefix = aliases[val]
+            else:
+                prefix = deepgetattr(node, 'value.id')
+        except Exception:
+            # We can't get the fully qualified name for an attr, just return
+            # its base name.
+            pass
+        
+        return "{0}.{1}".format(prefix, node.attr)
+    else:
+        return ""
+
+
+def deepgetattr(obj, attr):
+    """
+    Function to recurs through an attribute chain to get the ultimate value.
+    
+    @param attr attribute chain to be parsed
+    @type ast.Attribute
+    @return ultimate value
+    @rtype ast.AST
+    """
+    for key in attr.split('.'):
+        obj = getattr(obj, key)
+    return obj
+
+
+def linerange(node):
+    """
+    Function to get line number range from a node.
+    
+    @param node node to extract a line range from
+    @type ast.AST
+    @return list containing the line number range
+    @rtype list of int
+    """
+    strip = {"body": None, "orelse": None,
+             "handlers": None, "finalbody": None}
+    for key in strip.keys():
+        if hasattr(node, key):
+            strip[key] = getattr(node, key)
+            node.key = []
+    
+    lines_min = 9999999999
+    lines_max = -1
+    for n in ast.walk(node):
+        if hasattr(n, 'lineno'):
+            lines_min = min(lines_min, n.lineno)
+            lines_max = max(lines_max, n.lineno)
+    
+    for key in strip.keys():
+        if strip[key] is not None:
+            node.key = strip[key]
+    
+    if lines_max > -1:
+        return list(range(lines_min, lines_max + 1))
+    
+    return [0, 1]
+
+
+def linerange_fix(node):
+    """
+    Function to get a line number range working around a known Python bug
+    with multi-line strings.
+    
+    @param node node to extract a line range from
+    @type ast.AST
+    @return list containing the line number range
+    @rtype list of int
+    """
+    # deal with multiline strings lineno behavior (Python issue #16806)
+    lines = linerange(node)
+    if (
+        hasattr(node, '_securitySibling') and
+        hasattr(node._securitySibling, 'lineno')
+    ):
+        start = min(lines)
+        delta = node._securitySibling.lineno - start
+        if delta > 1:
+            return list(range(start, node._securitySibling.lineno))
+    
+    return lines
+
+
+def escapedBytesRepresentation(b):
+    """
+    Function to escape bytes for comparison with other strings.
+    
+    In practice it turns control characters into acceptable codepoints then
+    encodes them into bytes again to turn unprintable bytes into printable
+    escape sequences.
+
+    This is safe to do for the whole range 0..255 and result matches
+    unicode_escape on a unicode string.
+    
+    @param b bytes object to be escaped
+    @type bytes
+    @return escaped bytes object
+    @rtype bytes
+    """
+    return b.decode('unicode_escape').encode('unicode_escape')

eric ide

mercurial