Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DEFVAL improvements #11

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pysmi/codegen/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,11 @@ def gen_index(self, mibsMap, **kwargs):

@staticmethod
def is_binary(s):
return isinstance(s, str) and s[0] == "'" and s[-2:] in ("'b", "'B")
return isinstance(s, str) and s and s[0] == "'" and s[-2:] in ("'b", "'B")

@staticmethod
def is_hex(s):
return isinstance(s, str) and s[0] == "'" and s[-2:] in ("'h", "'H")
return isinstance(s, str) and s and s[0] == "'" and s[-2:] in ("'h", "'H")

def str2int(self, s):
if self.is_binary(s):
Expand Down
294 changes: 194 additions & 100 deletions pysmi/codegen/intermediate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@
from pysmi.codegen.base import AbstractCodeGen
from pysmi.mibinfo import MibInfo

if sys.version_info[0] > 2:
unicode = str
long = int


class IntermediateCodeGen(AbstractCodeGen):
"""Turns MIB AST into an intermediate representation.
Expand Down Expand Up @@ -220,7 +216,23 @@ def get_base_type(self, symName, module):

else:
baseSymType, baseSymSubtype = self.get_base_type(*symType)
if isinstance(baseSymSubtype, list):

if isinstance(baseSymSubtype, dict):
# An enumeration (INTEGER or BITS). Combine the enumeration
# lists when applicable. That is a bit more permissive than
# strictly needed, as syntax refinement may only remove entries
# from the base enumeration (RFC 2578 Sec. 9 point (2)).
if isinstance(symSubtype, dict):
baseSymSubtype.update(symSubtype)
symSubtype = baseSymSubtype

elif isinstance(baseSymSubtype, list):
# A value range or size constraint. Note that each list is an
# intersection of unions of ranges. Taking the intersection
# instead of the most-top level union of ranges is a bit more
# restrictive than strictly needed, as range syntax refinement
# may only remove allowed values from the base type (RFC 2578
# Sec. 9. points (1) and (3)), but it matches what pyasn1 does.
if isinstance(symSubtype, list):
symSubtype += baseSymSubtype
else:
Expand Down Expand Up @@ -486,7 +498,7 @@ def gen_object_type(self, data):
oidStr, parentOid = oid
indexStr, fakeSyms, fakeSymDicts = index or ("", [], [])

defval = self.gen_def_val(defval, objname=pysmiName)
defval = self.process_defval(defval, objname=pysmiName)

outDict = OrderedDict()
outDict["name"] = name
Expand Down Expand Up @@ -667,125 +679,207 @@ def gen_contact_info(self, data):
text = data[0]
return self.textFilter("contact-info", text)

def is_in_range(self, intersection, value):
"""Check whether the given value falls within the given constraints.

The given list represents an intersection and contains elements that
represent unions of individual ranges. Each individual range consists
of one or two elements (a single value, or a low..high range). The
given value is considered in range if it falls within range for each of
the unions within the intersection. If the intersection is empty, the
value is accepted as well.
"""
for union in intersection:
in_range = False
for rng in union:
if self.str2int(rng[0]) <= value <= self.str2int(rng[-1]):
in_range = True
break
if not in_range:
return False
return True

# noinspection PyUnusedLocal
def gen_display_hint(self, data):
return data[0]

# noinspection PyUnusedLocal
def gen_def_val(self, data, objname=None):
if not data:
return {}
# noinspection PyMethodMayBeStatic,PyUnusedLocal
def gen_def_val(self, data):
return data[0]

if not objname:
return data
def _process_integer_defval(self, defval, defvalType):
"""Process a DEFVAL value for an integer/enumeration type."""
if isinstance(defval, int): # decimal
value = defval

defval = data[0]
defvalType = self.get_base_type(objname, self.moduleName[0])
elif self.is_hex(defval): # hex
value = int(defval[1:-2] or "0", 16)

elif self.is_binary(defval): # binary
value = int(defval[1:-2] or "0", 2)

elif isinstance(defvalType[1], dict): # enumeration label
# For enumerations, the ASN.1 DEFVAL statements contain names,
# whereas the code generation template expects integer values
# (represented as strings).
nameToValueMap = defvalType[1]

# buggy MIB: DEFVAL { { ... } }
if isinstance(defval, list):
defval = [dv for dv in defval if dv in nameToValueMap]
if defval:
defval = defval[0]
# (fall through)

# good MIB: DEFVAL { ... }
if defval not in nameToValueMap:
raise ValueError("unknown enumeration label")

# Return early so as to skip the enumeration value check below.
# After all, we already know the resulting number is valid.
return str(nameToValueMap[defval]), "decimal"

else:
raise ValueError("wrong input type for integer")

if isinstance(defvalType[1], dict): # enumeration number
# For numerical values given for enumerated integers, make sure
# that they are valid, because pyasn1 will not check this case and
# thus let us set a default value that is not valid for the type.
if value not in defvalType[1].values():
raise ValueError("wrong enumeration value")

elif isinstance(defvalType[1], list): # range constraints
if not self.is_in_range(defvalType[1], value):
raise ValueError("value does not conform to range constraints")

return str(value), "decimal"

def _process_string_defval(self, defval, defvalType):
"""Process a DEFVAL value for a string type (including BITS)."""
defvalBaseType = defvalType[0][0] # either "OctetString" or "Bits"

outDict = OrderedDict(basetype=defvalType[0][0])
if isinstance(defval, int): # decimal
if defvalBaseType != "Bits":
raise ValueError("decimal values have no meaning for OCTET STRING")

if isinstance(defval, (int, long)): # number
outDict.update(value=defval, format="decimal")
# Convert the value to a hex string. Add padding if needed.
value = defval and hex(defval)[2:] or ""
value = ("0" + value) if len(value) % 2 == 1 else value

fmt = "hex"

elif self.is_hex(defval): # hex
# common bug in MIBs
if defvalType[0][0] in ("Integer32", "Integer"):
outDict.update(
value=str(int(len(defval) > 3 and defval[1:-2] or "0", 16)),
format="hex",
)
# Extract the value as hex string. Add padding if needed.
value = defval[1:-2]
value = ("0" + value) if len(value) % 2 == 1 else value

else:
outDict.update(value=defval[1:-2], format="hex")
fmt = "hex"

elif self.is_binary(defval): # binary
binval = defval[1:-2]

# common bug in MIBs
if defvalType[0][0] in ("Integer32", "Integer"):
outDict.update(value=str(int(binval or "0", 2)), format="bin")
# Make sure not to lose any leading zeroes. Add padding if needed.
width = ((len(binval) + 7) // 8) * 2
value = width and "{:0{width}x}".format(int(binval, 2), width=width) or ""
fmt = "hex"

else:
hexval = binval and hex(int(binval, 2))[2:] or ""
outDict.update(value=hexval, format="hex")
elif defval and defval[0] == '"' and defval[-1] == '"': # quoted string
if defvalBaseType != "OctetString":
raise ValueError("quoted strings have no meaning for BITS")

value = defval[1:-1]
fmt = "string"

elif defvalBaseType == "Bits" and isinstance(defval, list): # bit labels
defvalBits = []

# quoted string
elif defval and defval[0] == defval[-1] and defval[0] == '"':
# common bug in MIBs
if defval[1:-1] == "" and defvalType != "OctetString":
# a warning should be here
return {} # we will set no default value
bits = defvalType[1]

outDict.update(value=defval[1:-1], format="string")
for bit in defval:
bitValue = bits.get(bit, None)
if bitValue is not None:
defvalBits.append((bit, bitValue))
else:
raise ValueError("unknown bit")

return self.gen_bits([defvalBits])[1], "bits"

# symbol (oid as defval) or name for enumeration member
else:
# oid
if defvalType[0][0] == "ObjectIdentifier" and (
self.trans_opers(defval) in self.symbolTable[self.moduleName[0]]
or self.trans_opers(defval) in self._importMap
):
pysmiDefval = self.trans_opers(defval)
module = self._importMap.get(pysmiDefval, self.moduleName[0])

try:
val = str(
self.gen_numeric_oid(
self.symbolTable[module][pysmiDefval]["oid"]
)
)
raise ValueError("wrong input type for string")

outDict.update(value=val, format="oid")
if defvalBaseType == "OctetString" and isinstance(
defvalType[1], list
): # size constraints
size = len(value) // 2 if fmt == "hex" else len(value)

except Exception:
# or no module if it will be borrowed later
raise error.PySmiSemanticError(
f'no symbol "{defval}" in module "{module}"'
)
if not self.is_in_range(defvalType[1], size):
raise ValueError("value does not conform to size constraints")

# enumeration
elif defvalType[0][0] in ("Integer32", "Integer") and isinstance(
defvalType[1], list
):
# For enumerations, the ASN.1 DEFVAL statements contain names,
# whereas the code generation template expects integer values
# (represented as strings).
nameToValueMap = dict(defvalType[1])

# buggy MIB: DEFVAL { { ... } }
if isinstance(defval, list):
defval = [dv for dv in defval if dv in nameToValueMap]
if defval:
outDict.update(
value=str(nameToValueMap[defval[0]]), format="enum"
)

# good MIB: DEFVAL { ... }
elif defval in nameToValueMap:
outDict.update(value=str(nameToValueMap[defval]), format="enum")

elif defvalType[0][0] == "Bits":
defvalBits = []

bits = dict(defvalType[1])

for bit in defval:
bitValue = bits.get(bit, None)
if bitValue is not None:
defvalBits.append((bit, bitValue))
return value, fmt

else:
raise error.PySmiSemanticError(
f'no such bit as "{bit}" for symbol "{objname}"'
)
def _process_oid_defval(self, defval, defvalType):
"""Process a DEFVAL value for an object identifier."""
if isinstance(defval, str) and (
self.trans_opers(defval) in self.symbolTable[self.moduleName[0]]
or self.trans_opers(defval) in self._importMap
):
pysmiDefval = self.trans_opers(defval)
module = self._importMap.get(pysmiDefval, self.moduleName[0])

outDict.update(value=self.gen_bits([defvalBits])[1], format="bits")
value = self.gen_numeric_oid(self.symbolTable[module][pysmiDefval]["oid"])

else:
raise error.PySmiSemanticError(
f'unknown type "{defvalType}" for defval "{defval}" of symbol "{objname}"'
)
return str(value), "oid"

else:
raise ValueError("wrong input type for object identifier")

def process_defval(self, defval, objname):
if defval is None:
return {}

defvalType = self.get_base_type(objname, self.moduleName[0])
defvalBaseType = defvalType[0][0]

# Our general policy is that DEFVAL values are considered discardable:
# any values that were accepted by the parser but turn out to be
# invalid, are dropped here, so that they will not keep the MIB from
# being compiled or loaded. The underlying idea is that DEFVAL values
# are not all that important, and therefore better discarded than the
# cause of a MIB loading failure.
#
# For each combination of input value and object type, the following
# table shows whether the combination is:
# - required to be supported by RFC 2578;
# - accepted out of lenience by our implementation; or,
# - discarded for being no meaningful combination.
# Note that enumerations are also Integer32/Integer types, but handled
# slightly differently, so they are a separate column in this table.
#
# Input Integer Enumeration OctetString ObjectId. Bits
# ------------- ----------- ----------- ----------- ----------- ---------
# decimal required accepted discarded discarded accepted
# hex accepted accepted required discarded accepted
# binary accepted accepted required discarded accepted
# quoted string discarded discarded required discarded discarded
# symbol/label discarded required discarded required discarded
# brackets discarded accepted discarded discarded required

try:
if defvalBaseType in ("Integer32", "Integer"):
value, fmt = self._process_integer_defval(defval, defvalType)
elif defvalBaseType in ("OctetString", "Bits"):
value, fmt = self._process_string_defval(defval, defvalType)
elif defvalBaseType == "ObjectIdentifier":
value, fmt = self._process_oid_defval(defval, defvalType)
else: # pragma: no cover
raise ValueError("unknown base type")

except ValueError:
# Discard the given default value.
return {}

outDict = OrderedDict(basetype=defvalBaseType, value=value, format=fmt)
return {"default": outDict}

# noinspection PyMethodMayBeStatic
Expand Down Expand Up @@ -903,11 +997,11 @@ def gen_oid(self, data):
out = ()
parent = ""
for el in data[0]:
if isinstance(el, (str, unicode)):
if isinstance(el, str):
parent = self.trans_opers(el)
out += ((parent, self._importMap.get(parent, self.moduleName[0])),)

elif isinstance(el, (int, long)):
elif isinstance(el, int):
out += (el,)

elif isinstance(el, tuple):
Expand Down
Loading
Loading