# Source code for snimpy.mib

    #
    # snimpy -- Interactive SNMP tool
    #
    # Copyright (C) Vincent Bernat <bernat@luffy.cx>
    #
    # Permission to use, copy, modify, and/or distribute this software for any
    # purpose with or without fee is hereby granted, provided that the above
    # copyright notice and this permission notice appear in all copies.
    #
    # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
    # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
    # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
    #

    """This module is a low-level interface to manipulate and
    extract information from MIB files. It is a CFFI_ wrapper around
    libsmi_. You may find convenient to use it in other projects but the
    wrapper is merely here to serve *Snimpy* use and is therefore
    incomplete.

    .. _libsmi: http://www.ibr.cs.tu-bs.de/projects/libsmi/
    .. _CFFI: http://cffi.readthedocs.io/
    """

    try:
        from snimpy._smi import lib as _smi
        from snimpy._smi import ffi
    except ImportError:
        from snimpy.smi_build import ffi, get_lib
        _smi = get_lib()

    class SMIException(Exception):

        """SMI related exception. Any exception thrown in this module is
        inherited from this one."""

    class Node(object):

        """MIB node. An instance of this class represents a MIB node. It
        can be specialized by other classes, like :class:`Scalar`,
        :class:`Table`, :class:`Column`, :class:`Node`.
        """

        def __init__(self, node):
            """Create a new MIB node.

            :param node: libsmi node supporting this node.
            """
            self.node = node
            self._override_type = None

        @property
        def type(self):
            """Get the basic type associated with this node.

            :return: The class from :mod:`basictypes` module which can
                represent the node. When retrieving a valid value for
                this node, the returned class can be instanciated to get
                an appropriate representation.
            """
            from snimpy import basictypes
            if self._override_type:
                t = self._override_type
            else:
                t = _smi.smiGetNodeType(self.node)
            if t == ffi.NULL:
                raise SMIException("unable to retrieve type of node")
            target = {
                _smi.SMI_BASETYPE_INTEGER32: basictypes.Integer,
                _smi.SMI_BASETYPE_INTEGER64: basictypes.Integer,
                _smi.SMI_BASETYPE_UNSIGNED32: {b"TimeTicks": basictypes.Timeticks,
                                               None: basictypes.Unsigned32},
                _smi.SMI_BASETYPE_UNSIGNED64: basictypes.Unsigned64,
                _smi.SMI_BASETYPE_OCTETSTRING: {b"IpAddress": basictypes.IpAddress,
                                                None: basictypes.OctetString},
                _smi.SMI_BASETYPE_OBJECTIDENTIFIER: basictypes.Oid,
                _smi.SMI_BASETYPE_ENUM: {b"TruthValue": basictypes.Boolean,
                                         None: basictypes.Enum},
                _smi.SMI_BASETYPE_BITS: basictypes.Bits
            }.get(t.basetype, None)
            if isinstance(target, dict):
                tt = _smi.smiGetParentType(t)
                target = target.get((t.name != ffi.NULL and ffi.string(t.name)) or
                                    (tt.name != ffi.NULL and ffi.string(
                                        tt.name)) or None,
                                    target.get(None, None))

            if target is None:
                raise SMIException("unable to retrieve type of node")
            return target

        @property
        def typeName(self):
            """Retrieves the name of the the node's current declared type
            (not basic type).

            :return: A string representing the current declared type,
                suitable for assignment to type.setter.
            """
            if self._override_type:
                t = self._override_type
            else:
                t = _smi.smiGetNodeType(self.node)

            # This occurs when the type is "implied".
            if t.name == ffi.NULL:
                t = _smi.smiGetParentType(t)

            if t is None or t == ffi.NULL:
                raise SMIException("unable to retrieve the declared type "
                                   "of the node '{0}'".format(self.node.name))

            return ffi.string(t.name)

        @typeName.setter
        def typeName(self, type_name):
            """Override the node's type to type_name, found using _getType.
            The new type must resolve to the same basictype.

            :param type_name: string name of the type.
            """
            current_override = self._override_type

            declared_type = _smi.smiGetNodeType(self.node)
            declared_basetype = self.type

            new_type = _getType(type_name)
            if not new_type:
                raise SMIException("no type named {0} in any loaded module".format(
                    type_name))

            # Easiest way to find the new basetype is to set the override
            # and ask.
            self._override_type = new_type
            new_basetype = self.type

            if declared_basetype != new_basetype:
                self._override_type = current_override
                raise SMIException("override type {1} not compatible with "
                                   "basetype of {0}".format(
                                       ffi.string(declared_type.name),
                                       ffi.string(new_type.name)))

        @typeName.deleter
        def typeName(self):
            """Clears the type override."""
            self._override_type = None

        @property
        def fmt(self):
            """Get node format. The node format is a string to use to display
            a user-friendly version of the node. This is can be used for
            both octet strings or integers (to make them appear as decimal
            numbers).

            :return: The node format as a string or None if there is no
                format available.

            """
            if self._override_type:
                t = self._override_type
            else:
                t = _smi.smiGetNodeType(self.node)
            tt = _smi.smiGetParentType(t)
            f = (t != ffi.NULL and t.format != ffi.NULL and ffi.string(t.format) or
                 tt != ffi.NULL and tt.format != ffi.NULL and
                 ffi.string(tt.format)) or None
            if f is None:
                return None
            return f.decode("ascii")

        @property
        def oid(self):
            """Get OID for the current node. The OID can then be used to request
            the node from an SNMP agent.

            :return: OID as a tuple
            """
            return tuple([self.node.oid[i] for i in range(self.node.oidlen)])

        @property
        def ranges(self):
            """Get node ranges. An node can be restricted by a set of
            ranges. For example, an integer can only be provided between
            two values. For strings, the restriction is on the length of
            the string.

            The returned value can be `None` if no restriction on range
            exists for the current node, a single value if the range is
            fixed or a list of tuples or fixed values otherwise.

            :return: The valid range for this node.
            """
            t = _smi.smiGetNodeType(self.node)
            if t == ffi.NULL:
                return None

            ranges = []
            range = _smi.smiGetFirstRange(t)
            while range != ffi.NULL:
                m1 = self._convert(range.minValue)
                m2 = self._convert(range.maxValue)
                if m1 == m2:
                    ranges.append(m1)
                else:
                    ranges.append((m1, m2))
                range = _smi.smiGetNextRange(range)
            if len(ranges) == 0:
                return None
            if len(ranges) == 1:
                return ranges[0]
            return ranges

        @property
        def enum(self):
            """Get possible enum values. When the node can only take a discrete
            number of values, those values are defined in the MIB and can
            be retrieved through this property.

            :return: The dictionary of possible values keyed by the integer value.
            """
            t = _smi.smiGetNodeType(self.node)
            if t == ffi.NULL or t.basetype not in (_smi.SMI_BASETYPE_ENUM,
                                                   _smi.SMI_BASETYPE_BITS):
                return None

            result = {}
            element = _smi.smiGetFirstNamedNumber(t)
            while element != ffi.NULL:
                result[self._convert(element.value)] = ffi.string(
                    element.name).decode("ascii")
                element = _smi.smiGetNextNamedNumber(element)
            return result

        def __str__(self):
            return ffi.string(self.node.name).decode("ascii")

        def __repr__(self):
            r = _smi.smiRenderNode(self.node, _smi.SMI_RENDER_ALL)
            if r == ffi.NULL:
                return "<uninitialized {0} object at {1}>".format(
                    self.__class__.__name__, hex(id(self)))
            r = ffi.gc(r, _smi.free)
            module = _smi.smiGetNodeModule(self.node)
            if module == ffi.NULL:
                raise SMIException("unable to get module for {0}".format(
                    self.node.name))
            return "<{0} {1} from '{2}'>".format(self.__class__.__name__,
                                                 ffi.string(r),
                                                 ffi.string(module.name))

        def _convert(self, value):
            attr = {_smi.SMI_BASETYPE_INTEGER32: "integer32",
                    _smi.SMI_BASETYPE_UNSIGNED32: "unsigned32",
                    _smi.SMI_BASETYPE_INTEGER64: "integer64",
                    _smi.SMI_BASETYPE_UNSIGNED64: "unsigned64"}.get(value.basetype,
                                                                    None)
            if attr is None:
                raise SMIException("unexpected type found in range")
            return getattr(value.value, attr)

    class Scalar(Node):

        """MIB scalar node. This class represents a scalar value in the
        MIB. A scalar value is a value not contained in a table.
        """

    class Table(Node):

        """MIB table node. This class represents a table. A table is an
        ordered collection of objects consisting of zero or more
        rows. Each object in the table is identified using an index. An
        index can be a single value or a list of values.
        """

        @property
        def columns(self):
            """Get table columns. The columns are the different kind of objects
            that can be retrieved in a table.

            :return: list of table columns (:class:`Column` instances)

            """
            child = _smi.smiGetFirstChildNode(self.node)
            if child == ffi.NULL:
                return []
            if child.nodekind != _smi.SMI_NODEKIND_ROW:
                raise SMIException("child {0} of {1} is not a row".format(
                    ffi.string(child.name),
                    ffi.string(self.node.name)))
            columns = []
            child = _smi.smiGetFirstChildNode(child)
            while child != ffi.NULL:
                if child.nodekind != _smi.SMI_NODEKIND_COLUMN:
                    raise SMIException("child {0} of {1} is not a column".format(
                        ffi.string(child.name),
                        ffi.string(self.node.name)))
                columns.append(Column(child))
                child = _smi.smiGetNextChildNode(child)
            return columns

        @property
        def _row(self):
            """Get the table row.

            :return: row object (as an opaque object)
            """
            child = _smi.smiGetFirstChildNode(self.node)
            if child != ffi.NULL and child.indexkind == _smi.SMI_INDEX_AUGMENT:
                child = _smi.smiGetRelatedNode(child)
                if child == ffi.NULL:
                    raise SMIException("AUGMENT index for {0} but "
                                       "unable to retrieve it".format(
                                           ffi.string(self.node.name)))
            if child == ffi.NULL:
                raise SMIException("{0} does not have a row".format(
                    ffi.string(self.node.name)))
            if child.nodekind != _smi.SMI_NODEKIND_ROW:
                raise SMIException("child {0} of {1} is not a row".format(
                    ffi.string(child.name),
                    ffi.string(self.node.name)))
            if child.indexkind != _smi.SMI_INDEX_INDEX:
                raise SMIException("child {0} of {1} has an unhandled "
                                   "kind of index".format(
                                       ffi.string(child.name),
                                       ffi.string(self.node.name)))
            return child

        @property
        def implied(self):
            """Is the last index implied? An implied index is an index whose size
            is not fixed but who is not prefixed by its size because this
            is the last index of a table.

            :return: `True` if and only if the last index is implied.
            """
            child = self._row
            if child.implied:
                return True
            return False

        @property
        def index(self):
            """Get indexes for a table. The indexes are used to locate a precise
            row in a table. They are a subset of the table columns.

            :return: The list of indexes (as :class:`Column` instances) of
                the table.
            """
            child = self._row
            lindex = []
            element = _smi.smiGetFirstElement(child)
            while element != ffi.NULL:
                nelement = _smi.smiGetElementNode(element)
                if nelement == ffi.NULL:
                    raise SMIException("cannot get index "
                                       "associated with {0}".format(
                                           ffi.string(self.node.name)))
                if nelement.nodekind != _smi.SMI_NODEKIND_COLUMN:
                    raise SMIException("index {0} for {1} is "
                                       "not a column".format(
                                           ffi.string(nelement.name),
                                           ffi.string(self.node.name)))
                lindex.append(Column(nelement))
                element = _smi.smiGetNextElement(element)
            return lindex

    class Column(Node):

        """MIB column node. This class represent a column of a table."""

        @property
        def table(self):
            """Get table associated with this column.

            :return: The :class:`Table` instance associated to this
                column.
            """
            parent = _smi.smiGetParentNode(self.node)
            if parent == ffi.NULL:
                raise SMIException("unable to get parent of {0}".format(
                    ffi.string(self.node.name)))
            if parent.nodekind != _smi.SMI_NODEKIND_ROW:
                raise SMIException("parent {0} of {1} is not a row".format(
                    ffi.string(parent.name),
                    ffi.string(self.node.name)))
            parent = _smi.smiGetParentNode(parent)
            if parent == ffi.NULL:
                raise SMIException("unable to get parent of {0}".format(
                    ffi.string(self.node.name)))
            if parent.nodekind != _smi.SMI_NODEKIND_TABLE:
                raise SMIException("parent {0} of {1} is not a table".format(
                    ffi.string(parent.name),
                    ffi.string(self.node.name)))
            t = Table(parent)
            return t

    class Notification(Node):

        """MIB notification node. This class represent a notification."""

        @property
        def objects(self):
            """Get objects for a notification.

            :return: The list of objects (as :class:`Column`,
                :class:`Node` or :class:`Scalar` instances) of the notification.
            """
            child = self.node
            lindex = []
            element = _smi.smiGetFirstElement(child)
            while element != ffi.NULL:
                nelement = _smi.smiGetElementNode(element)
                if nelement == ffi.NULL:
                    raise SMIException("cannot get object "
                                       "associated with {0}".format(
                                           ffi.string(self.node.name)))
                if nelement.nodekind == _smi.SMI_NODEKIND_COLUMN:
                    lindex.append(Column(nelement))
                elif nelement.nodekind == _smi.SMI_NODEKIND_NODE:
                    lindex.append(Node(nelement))
                elif nelement.nodekind == _smi.SMI_NODEKIND_SCALAR:
                    lindex.append(Scalar(nelement))
                else:
                    raise SMIException("object {0} for {1} is "
                                       "not a node".format(
                                           ffi.string(nelement.name),
                                           ffi.string(self.node.name)))
                element = _smi.smiGetNextElement(element)
            return lindex

    _lastError = None

    @ffi.callback("void(char *, int, int, char *, char*)")
    def _logError(path, line, severity, msg, tag):
        global _lastError
        if path != ffi.NULL and msg != ffi.NULL:
            _lastError = "{0}:{1}: {2}".format(ffi.string(path), line,
                                               ffi.string(msg))
        else:
            _lastError = None

    def reset():
        """Reset libsmi to its initial state."""
        _smi.smiExit()
        if _smi.smiInit(b"snimpy") < 0:
                raise SMIException("unable to init libsmi")
        _smi.smiSetErrorLevel(1)
        _smi.smiSetErrorHandler(_logError)
        try:
            _smi.smiSetFlags(_smi.SMI_FLAG_ERRORS | _smi.SMI_FLAG_RECURSIVE)
        except TypeError:
            pass                    # We are being mocked

    def path(path=None):
        """Set or get a search path to libsmi.

        When no path is provided, return the current path,
        unmodified. Otherwise, set the path to the specified value.

        :param path: The string to be used to change the search path or
                     `None`

        """
        if path is None:
            # Get the path
            path = _smi.smiGetPath()
            if path == ffi.NULL:
                raise SMIException("unable to get current libsmi path")
            path = ffi.gc(path, _smi.free)
            result = ffi.string(path)
            return result.decode("utf8")

        # Set the path
        if not isinstance(path, bytes):
            path = path.encode("utf8")
        if _smi.smiSetPath(path) < 0:
            raise SMIException("unable to set the path {0}".format(path))

    def _get_module(name):
        """Get the SMI module from its name.

        :param name: The name of the module
        :return: The SMI module or `None` if not found (not loaded)
        """
        if not isinstance(name, bytes):
            name = name.encode("ascii")
        m = _smi.smiGetModule(name)
        if m == ffi.NULL:
            return None
        if m.conformance and m.conformance <= 1:
            return None
        return m

    def _kind2object(kind):
        return {
            _smi.SMI_NODEKIND_NODE: Node,
            _smi.SMI_NODEKIND_SCALAR: Scalar,
            _smi.SMI_NODEKIND_TABLE: Table,
            _smi.SMI_NODEKIND_NOTIFICATION: Notification,
            _smi.SMI_NODEKIND_COLUMN: Column
        }.get(kind, Node)

    def get(mib, name):
        """Get a node by its name.

        :param mib: The MIB name to query
        :param name: The object name to get from the MIB
        :return: the requested MIB node (:class:`Node`)
        """
        if not isinstance(mib, bytes):
            mib = mib.encode("ascii")
        module = _get_module(mib)
        if module is None:
            raise SMIException("no module named {0}".format(mib))
        node = _smi.smiGetNode(module, name.encode("ascii"))
        if node == ffi.NULL:
            raise SMIException("in {0}, no node named {1}".format(
                mib, name))
        pnode = _kind2object(node.nodekind)
        return pnode(node)

    def getByOid(oid):
        """Get a node by its OID.

        :param oid: The OID as a tuple
        :return: The requested MIB node (:class:`Node`)
        """
        node = _smi.smiGetNodeByOID(len(oid), oid)
        if node == ffi.NULL:
            raise SMIException("no node for {0}".format(
                ".".join([str(o) for o in oid])))
        pnode = _kind2object(node.nodekind)
        return pnode(node)

    def _getType(type_name):
        """Searches for a smi type through all loaded modules.

        :param type_name: The name of the type to search for.
        :return: The requested type (:class:`smi.SmiType`), if found, or None.
        """
        if not isinstance(type_name, bytes):
            type_name = type_name.encode("ascii")
        for module in _loadedModules():
            new_type = _smi.smiGetType(module, type_name)
            if new_type != ffi.NULL:
                return new_type
        return None

    def _get_kind(mib, kind):
        """Get nodes of a given kind from a MIB.

        :param mib: The MIB name to search objects for
        :param kind: The SMI kind of object
        :return: The list of matched MIB nodes for the MIB
        """
        if not isinstance(mib, bytes):
            mib = mib.encode("ascii")
        module = _get_module(mib)
        if module is None:
            raise SMIException("no module named {0}".format(mib))
        lnode = []
        node = _smi.smiGetFirstNode(module, kind)
        while node != ffi.NULL:
            lnode.append(_kind2object(kind)(node))
            node = _smi.smiGetNextNode(node, kind)
        return lnode

    def getNodes(mib):
        """Return all nodes from a given MIB.

        :param mib: The MIB name
        :return: The list of all MIB nodes for the MIB
        :rtype: list of :class:`Node` instances
        """
        return _get_kind(mib, _smi.SMI_NODEKIND_NODE)

    def getScalars(mib):
        """Return all scalars from a given MIB.

        :param mib: The MIB name
        :return: The list of all scalars for the MIB
        :rtype: list of :class:`Scalar` instances
        """
        return _get_kind(mib, _smi.SMI_NODEKIND_SCALAR)

    def getTables(mib):
        """Return all tables from a given MIB.

        :param mib: The MIB name
        :return: The list of all tables for the MIB
        :rtype: list of :class:`Table` instances
        """
        return _get_kind(mib, _smi.SMI_NODEKIND_TABLE)

    def getColumns(mib):
        """Return all columns from a givem MIB.

        :param mib: The MIB name
        :return: The list of all columns for the MIB
        :rtype: list of :class:`Column` instances
        """
        return _get_kind(mib, _smi.SMI_NODEKIND_COLUMN)

    def getNotifications(mib):
        """Return all notifications from a givem MIB.

        :param mib: The MIB name
        :return: The list of all notifications for the MIB
        :rtype: list of :class:`Notification` instances
        """
        return _get_kind(mib, _smi.SMI_NODEKIND_NOTIFICATION)

    def load(mib):
        """Load a MIB into the library.

        :param mib: The MIB to load, either a filename or a MIB name.
        :return: The MIB name that has been loaded.
        :except SMIException: The requested MIB cannot be loaded.
        """
        if not isinstance(mib, bytes):
            mib = mib.encode("ascii")
        modulename = _smi.smiLoadModule(mib)
        if modulename == ffi.NULL:
            raise SMIException("unable to find {0} (check the path)".format(mib))
        modulename = ffi.string(modulename)
        if not _get_module(modulename.decode("ascii")):
            details = "check with smilint -s -l1"
            if _lastError is not None:
                details = "{0}: {1}".format(_lastError,
                                            details)
            raise SMIException(
                "{0} contains major SMI error ({1})".format(mib, details))
        return modulename

    def _loadedModules():
        """Generates the list of loaded modules.

        :yield: The :class:`smi.SmiModule` of all currently loaded modules.
        """
        module = _smi.smiGetFirstModule()
        while module != ffi.NULL:
            yield module

            module = _smi.smiGetNextModule(module)

    def loadedMibNames():
        """Generates the list of loaded MIB names.

        :yield: The names of all currently loaded MIBs.
        """
        for module in _loadedModules():
            yield ffi.string(module.name).decode('utf-8')

    reset()