545 lines
22 KiB
Python
545 lines
22 KiB
Python
# -*- coding: UTF-8 -*-
|
|
#/**
|
|
# * Software Name : pycrate
|
|
# * Version : 0.3
|
|
# *
|
|
# * Copyright 2017. Benoit Michau. ANSSI.
|
|
# *
|
|
# * This library is free software; you can redistribute it and/or
|
|
# * modify it under the terms of the GNU Lesser General Public
|
|
# * License as published by the Free Software Foundation; either
|
|
# * version 2.1 of the License, or (at your option) any later version.
|
|
# *
|
|
# * This library is distributed in the hope that it will be useful,
|
|
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# * Lesser General Public License for more details.
|
|
# *
|
|
# * You should have received a copy of the GNU Lesser General Public
|
|
# * License along with this library; if not, write to the Free Software
|
|
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
# * MA 02110-1301 USA
|
|
# *
|
|
# *--------------------------------------------------------
|
|
# * File Name : pycrate_asn1rt/init.py
|
|
# * Created : 2017-01-31
|
|
# * Authors : Benoit Michau
|
|
# *--------------------------------------------------------
|
|
#*/
|
|
|
|
from .utils import *
|
|
from .glob import *
|
|
from .refobj import *
|
|
from .setobj import *
|
|
from .codecs import ASN1CodecBER
|
|
|
|
import inspect
|
|
|
|
def init_modules(*args, **kwargs):
|
|
"""
|
|
Generates the GLOBAL.MOD dict referencing all compiled objects
|
|
And for each defined objects:
|
|
- set the _parent attribute for objects inside constructed objects
|
|
- set the _root_*, _ext_*, _cont_tags
|
|
- translates the _typeref attribute from ASN1Ref to a ref to the current ASN1Obj instance
|
|
- bind content and constraints attributes to those from inherited types
|
|
|
|
args: the list of ASN.1 classes
|
|
kwargs:
|
|
GLOBAL: a specific GLOBAL dict, default is the generic GLOBAL
|
|
"""
|
|
if 'GLOBAL' in kwargs:
|
|
GLOB = kwargs['GLOBAL']
|
|
else:
|
|
GLOB = GLOBAL
|
|
for Mod in args:
|
|
GLOB.MOD[Mod._name_] = ASN1Dict()
|
|
GLOB.MOD[Mod._name_]['_oid_'] = Mod._oid_
|
|
GLOB.MOD[Mod._name_]['_obj_'] = Mod._obj_
|
|
if Mod.__name__[:1] != '_':
|
|
# do not process special modules _IMPL_ and _USER_
|
|
GLOB.MOD[Mod._name_]['_type_'] = Mod._type_
|
|
GLOB.MOD[Mod._name_]['_set_'] = Mod._set_
|
|
GLOB.MOD[Mod._name_]['_val_'] = Mod._val_
|
|
GLOB.MOD[Mod._name_]['_class_'] = Mod._class_
|
|
GLOB.MOD[Mod._name_]['_param_'] = Mod._param_
|
|
#
|
|
for objname in Mod._obj_:
|
|
GLOB.MOD[Mod._name_][objname] = getattr(Mod, name_to_defin(objname))
|
|
#
|
|
# set special attributes for some objects
|
|
for Mod in args:
|
|
for Obj in Mod._all_:
|
|
#
|
|
# useful for debugging...
|
|
Obj._mod = Mod.__name__
|
|
#
|
|
# setting additional attributes
|
|
if Obj.TYPE == TYPE_INT:
|
|
if Obj._cont is not None:
|
|
Obj._cont_rev = {Obj._cont[name]: name for name in Obj._cont}
|
|
if Obj._const_val:
|
|
Obj._const_val._set_root_bnd()
|
|
#
|
|
elif Obj.TYPE in TYPES_CONST_SZ:
|
|
if Obj._const_sz:
|
|
Obj._const_sz._set_root_bnd()
|
|
#
|
|
if Obj.TYPE == TYPE_BIT_STR:
|
|
if Obj._cont:
|
|
Obj._cont_rev = {Obj._cont[name]: name for name in Obj._cont}
|
|
#
|
|
if Obj.TYPE in TYPES_STRING and Obj._const_alpha:
|
|
Obj._const_alpha._set_root_bnd()
|
|
#
|
|
elif Obj.TYPE in (TYPE_BIT_STR, TYPE_OCT_STR) and Obj._const_cont is not None:
|
|
# set _const_cont_enc if not defined
|
|
if not hasattr(Obj, '_const_cont_enc'):
|
|
Obj._const_cont_enc = None
|
|
#
|
|
elif Obj.TYPE in (TYPE_SEQ_OF, TYPE_SET_OF) and Obj._cont is not None:
|
|
# set _parent for the component
|
|
Obj._cont._parent = Obj
|
|
#
|
|
elif Obj.TYPE == TYPE_ENUM and Obj._cont is not None:
|
|
# set _root
|
|
if not Obj._ext:
|
|
Obj._root = list(Obj._cont.keys())
|
|
else:
|
|
Obj._root = []
|
|
for name in Obj._cont:
|
|
if name not in Obj._ext:
|
|
Obj._root.append(name)
|
|
# set _cont_rev
|
|
Obj._cont_rev = {Obj._cont[name]: name for name in Obj._cont}
|
|
# set _const_ind
|
|
if Obj._ext is None:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)])
|
|
elif not Obj._ext:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)], ev=[])
|
|
else:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)], ev=[],
|
|
er=[ASN1RangeInt(0, len(Obj._ext)-1)])
|
|
Obj._const_ind._set_root_bnd()
|
|
#
|
|
elif Obj.TYPE in (TYPE_CHOICE, TYPE_SEQ, TYPE_SET, TYPE_CLASS) and Obj._cont is not None:
|
|
# set _parent for each component
|
|
for Comp in Obj._cont.values():
|
|
Comp._parent = Obj
|
|
#
|
|
if Obj.TYPE == TYPE_CHOICE:
|
|
# set _root, _const_ind
|
|
Obj._root, ext = [], []
|
|
if Obj._ext is not None:
|
|
ext = Obj._ext
|
|
for name in Obj._cont:
|
|
if name in ext:
|
|
break
|
|
else:
|
|
Obj._root.append(name)
|
|
if Obj._ext is None:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)])
|
|
elif not Obj._ext:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)], ev=[])
|
|
else:
|
|
Obj._const_ind = ASN1Set(rr=[ASN1RangeInt(0, len(Obj._root)-1)], ev=[],
|
|
er=[ASN1RangeInt(0, len(Obj._ext)-1)])
|
|
Obj._const_ind._set_root_bnd()
|
|
else:
|
|
# set _root, _root_mand, _root_opt
|
|
Obj._root, Obj._root_mand, Obj._root_opt, ext = [], [], [], []
|
|
if Obj._ext is not None:
|
|
ext = Obj._ext
|
|
for name, Comp in Obj._cont.items():
|
|
if name in ext:
|
|
break
|
|
if Comp._opt or Comp._def is not None:
|
|
Obj._root_opt.append(name)
|
|
else:
|
|
Obj._root_mand.append(name)
|
|
Obj._root.append(name)
|
|
#
|
|
if Obj.TYPE != TYPE_CLASS:
|
|
# set _ext_ident, _ext_group
|
|
if Obj._ext is not None:
|
|
Obj._ext_ident, Obj._ext_group = {}, {}
|
|
for name in Obj._ext:
|
|
Comp = Obj._cont[name]
|
|
if Comp._group is not None:
|
|
Obj._ext_ident[name] = Comp._group
|
|
if Comp._group not in Obj._ext_group:
|
|
Obj._ext_group[Comp._group] = []
|
|
Obj._ext_group[Comp._group].append(name)
|
|
#
|
|
if Obj.TYPE in (TYPE_SEQ, TYPE_SET) and Obj._ext is not None:
|
|
# set _ext_nest and _ext_group_obj
|
|
Obj._ext_nest, Obj._ext_group_obj = [], {}
|
|
for ident in Obj._ext:
|
|
if ident in Obj._ext_ident:
|
|
# ident is in a group
|
|
g_idents = Obj._ext_group[Obj._ext_ident[ident]]
|
|
if g_idents.index(ident) == 0:
|
|
# 1st component of the group
|
|
Obj._ext_nest.append( [ident] )
|
|
else:
|
|
Obj._ext_nest[-1].append(ident)
|
|
else:
|
|
Obj._ext_nest.append(ident)
|
|
#
|
|
for gid, idents in Obj._ext_group.items():
|
|
GSeq = Obj.__class__(name='%s_ext_%d' % (Obj._name, gid),
|
|
mode=MODE_TYPE)
|
|
GSeq._cont = ASN1Dict([(i, Obj._cont[i]) for i in idents])
|
|
GSeq._parent = Obj
|
|
GSeq._root = idents
|
|
GSeq._ext = None
|
|
GSeq._root_mand = [i for i in idents if Obj._cont[i]._opt is False and \
|
|
Obj._cont[i]._def is None]
|
|
GSeq._root_opt = [i for i in idents if i not in GSeq._root_mand]
|
|
# add a specific attribute
|
|
GSeq._gext = True
|
|
Obj._ext_group_obj[gid] = GSeq
|
|
#
|
|
elif Obj.TYPE == TYPE_OID and Obj._mode == MODE_VALUE:
|
|
if Obj._val in GLOB.OID and GLOB.OID[Obj._val] != Obj._name:
|
|
if not Obj._SILENT:
|
|
asnlog('init_modules: different OID objects (%s, %s) with same OID value %r'\
|
|
% (Obj._name, GLOB.OID[Obj._val], Obj._val))
|
|
elif Obj._val is not None:
|
|
GLOB.OID[Obj._val] = Obj._name
|
|
#
|
|
elif Obj.TYPE == TYPE_CLASS and Obj._mode == MODE_SET and Obj._val:
|
|
# this should not conflict with the previous check on TYPE_CLASS
|
|
# which must have self._cont defined (hence being MODE_TYPE)
|
|
build_classset_dict(Obj)
|
|
|
|
#
|
|
# lists all objects defined
|
|
Objs = [Obj for Mod in args for Obj in Mod._all_]
|
|
# lists all objects which inherits in some way from another one
|
|
TRObjs = [Obj for Obj in Objs if Obj._typeref is not None]
|
|
#
|
|
while TRObjs:
|
|
#asnlog('remaining objects: {0!r}'.format(len(Objs)))
|
|
for Obj in TRObjs:
|
|
try:
|
|
# resolve cross-reference
|
|
Obj._tr = get_typeref(Obj, GLOB)
|
|
except:
|
|
pass
|
|
else:
|
|
# this binding step is necessary in order to resolve ref to inner
|
|
# objects (ASN1RefClassField, ASN1RefChoiceComp, ...)
|
|
bind_all_attrs(Obj)
|
|
TRObjs.remove(Obj)
|
|
#
|
|
# When all typeref are resolved, we can set the tag chain and bind attributes
|
|
# for all objects
|
|
for Obj in Objs:
|
|
Obj._tagc = get_tag_chain(Obj)
|
|
if Obj._typeref is not None:
|
|
bind_all_attrs(Obj)
|
|
#
|
|
# We need another round, at least to set tag-object lookup properly for
|
|
# constructed object
|
|
for Obj in Objs:
|
|
if Obj.TYPE == TYPE_SEQ:
|
|
# set a list of tags for the content
|
|
Obj._cont_tags = [Cont._tagc[0] if Cont._tagc else None for Cont in Obj._cont.values()]
|
|
elif Obj.TYPE == TYPE_CHOICE:
|
|
# set an ASN1Dict of tags: objects for the content
|
|
Obj._cont_tags = get_cont_tags_dict(Obj)
|
|
elif Obj.TYPE == TYPE_SET:
|
|
# set an ASN1Dict of tags: objects for the content
|
|
Obj._cont_tags = get_cont_tags_dict(Obj)
|
|
# add the canonical list of root components according to their tag
|
|
Obj._root_canon = get_cont_tags_canon(Obj)
|
|
#
|
|
# additionally, we make safe checks on all generated objects
|
|
if Obj._SAFE_INIT:
|
|
Obj._safechk_obj()
|
|
if Obj._mode == MODE_VALUE:
|
|
try:
|
|
Obj._safechk_val(Obj._val)
|
|
except Exception as err:
|
|
# in case Obj is a field within a CLASS, val can be None
|
|
par = Obj._parent
|
|
while par and par.TYPE != TYPE_CLASS:
|
|
par = par._parent
|
|
if not par or par.TYPE != TYPE_CLASS:
|
|
raise(err)
|
|
elif Obj._val is not None:
|
|
raise(err)
|
|
elif Obj._mode == MODE_SET:
|
|
try:
|
|
Obj._safechk_set(Obj._val)
|
|
except Exception as err:
|
|
# in case Obj is a field within a CLASS, val can be None
|
|
par = Obj._parent
|
|
while par and par.TYPE != TYPE_CLASS:
|
|
par = par._parent
|
|
if not par or par.TYPE != TYPE_CLASS:
|
|
raise(err)
|
|
elif Obj._val is not None:
|
|
raise(err)
|
|
|
|
|
|
def get_typeref(Obj, GLOB=GLOBAL):
|
|
"""
|
|
returns the ASN.1 object corresponding to the typered (ASN1Ref) of Obj
|
|
"""
|
|
ref = Obj._typeref
|
|
#
|
|
if isinstance(ref, (ASN1RefType, ASN1RefInstOf)):
|
|
assert( ref.called is not None and ref.ced_path == [] )
|
|
tr = get_asnobj(ref, GLOB)
|
|
#
|
|
elif isinstance(ref, ASN1RefClassField):
|
|
assert( ref.called is not None and len(ref.ced_path) >= 1 )
|
|
cla = get_asnobj(ref, GLOB)
|
|
if cla._param:
|
|
# parameterized object are not compiled
|
|
return None
|
|
classpath = ref.ced_path[:]
|
|
while len(classpath) > 1:
|
|
tr = cla._cont[classpath[0]]
|
|
if tr._typeref is None:
|
|
# CLASS locally defined within CLASS
|
|
cla = tr
|
|
else:
|
|
cla = get_typeref(tr, GLOB)
|
|
del classpath[0]
|
|
tr = cla._cont[classpath[0]]
|
|
#
|
|
elif isinstance(ref, ASN1RefClassIntern):
|
|
assert( ref.called is None and len(ref.ced_path) >= 1 )
|
|
cla = self._parent
|
|
classpath = ref.ced_path[:]
|
|
while len(classpath) > 1:
|
|
tr = cla._cont[classpath[0]]
|
|
if tr._typeref is None:
|
|
# CLASS locally defined within CLASS
|
|
cla = tr
|
|
else:
|
|
cla = get_typeref(tr, GLOB)
|
|
del classpath[0]
|
|
tr = cla._cont[classpath[0]]
|
|
#
|
|
elif isinstance(ref, ASN1RefClassValField):
|
|
assert( ref.called is not None and len(ref.ced_path) >= 1 )
|
|
cla = get_asnobj(ref, GLOB)
|
|
if cla._param:
|
|
# parameterized object are not compiled
|
|
return None
|
|
claval = cla._val
|
|
classpath = ref.ced_path[:]
|
|
while len(classpath) > 0:
|
|
claval = claval[classpath[0]]
|
|
del classpath[0]
|
|
tr = claval
|
|
#
|
|
elif isinstance(ref, ASN1RefChoiceComp):
|
|
assert( ref.called is not None and len(ref.ced_path) >= 1 )
|
|
cho = get_asnobj(ref, GLOB)
|
|
if cho._param:
|
|
# parameterized object are not compiled
|
|
return None
|
|
choicepath = ref.ced_path[:]
|
|
while len(choicepath) > 1:
|
|
tr = cho._cont[choicepath[0]]
|
|
if tr._typeref is None:
|
|
# CHOICE locally defined within CHOICE
|
|
cho = tr
|
|
else:
|
|
cho = get_typeref(tr, GLOB)
|
|
del choicepath[0]
|
|
tr = cho._cont[choicepath[0]]
|
|
#
|
|
return tr
|
|
|
|
|
|
def get_tag_chain(Obj):
|
|
"""
|
|
returns the list of tags from self up to the last referred object
|
|
|
|
in case one tag is IMPLICIT, the next tag is not put in the list
|
|
"""
|
|
obj, tagc, impl = Obj, [], False
|
|
while obj is not None:
|
|
if obj._tag is not None:
|
|
if not impl:
|
|
tagc.append( (ASN1CodecBER.TagClassLUT[obj._tag[1]], obj._tag[0]) )
|
|
if obj._tag[2] == TAG_IMPLICIT:
|
|
impl = True
|
|
else:
|
|
impl = False
|
|
obj = obj._tr
|
|
if not impl and Obj.TYPE not in (TYPE_CHOICE, TYPE_OPEN, TYPE_ANY):
|
|
# add the universal tag
|
|
tagc.append( (0, Obj.TAG) )
|
|
return tagc
|
|
|
|
|
|
def get_cont_tags_dict(Obj):
|
|
"""
|
|
returns the ASN1Dict of tags, object for the content of Obj (SET and CHOICE)
|
|
"""
|
|
tagd = ASN1Dict()
|
|
for ident, Comp in Obj._cont.items():
|
|
if not Comp._tagc:
|
|
# untagged component
|
|
if Comp.TYPE == TYPE_CHOICE:
|
|
if hasattr(Comp, '_cont_tags'):
|
|
cho_tagd = Comp._cont_tags
|
|
else:
|
|
cho_tagd = get_cont_tags_dict(Comp)
|
|
for t in cho_tagd:
|
|
if t in tagd:
|
|
assert()
|
|
cho_ident = cho_tagd[t]
|
|
if isinstance(cho_ident, list):
|
|
tagd[t] = [Comp._name] + cho_ident
|
|
else:
|
|
tagd[t] = [Comp._name, cho_ident]
|
|
elif Comp.TYPE in (TYPE_OPEN, TYPE_ANY):
|
|
assert()
|
|
else:
|
|
assert()
|
|
elif Comp._tagc[0] in tagd:
|
|
assert()
|
|
else:
|
|
tagd[Comp._tagc[0]] = ident
|
|
return tagd
|
|
|
|
|
|
def get_cont_tags_canon(Obj):
|
|
"""
|
|
returns the list of components in the canonical order of their tags
|
|
"""
|
|
tagcan = ASN1Dict()
|
|
for ident in Obj._root:
|
|
Comp = Obj._cont[ident]
|
|
if not Comp._tagc:
|
|
# untagged component
|
|
if Comp.TYPE == TYPE_CHOICE:
|
|
cho_tags = get_cont_tags_dict(Comp)
|
|
# use the "lowest" tag of the dict to order it
|
|
cho_tag = sorted(cho_tags.keys())[0]
|
|
if cho_tag in tagcan:
|
|
assert()
|
|
tagcan[cho_tag] = ident
|
|
elif Comp.TYPE in (TYPE_OPEN, TYPE_ANY):
|
|
assert()
|
|
else:
|
|
assert()
|
|
elif Comp._tagc[0] in tagcan:
|
|
assert()
|
|
else:
|
|
tagcan[Comp._tagc[0]] = ident
|
|
# order the whole stuff
|
|
return [tagcan[k] for k in sorted(tagcan.keys())]
|
|
|
|
|
|
def bind_attrs(Obj, *attrs):
|
|
attr = attrs[0]
|
|
if getattr(Obj, attr) is None:
|
|
tr = Obj._tr if (Obj._tr is not None and not Obj._tr._param) else None
|
|
# look recursively into typeref
|
|
while tr is not None:
|
|
try:
|
|
v = getattr(tr, attr)
|
|
except Exception as err:
|
|
# this can happen when binding certain ASN.1 native types to
|
|
# SEQUENCE object in the _IMPL_ module
|
|
if tr._name in (TYPE_REAL, TYPE_EXT, TYPE_EMB_PDV, TYPE_CHAR_STR):
|
|
return
|
|
else:
|
|
raise(err)
|
|
if v is not None:
|
|
setattr(Obj, attr, v)
|
|
if len(attrs) > 1:
|
|
for a in attrs[1:]:
|
|
setattr(Obj, a, getattr(tr, a))
|
|
tr = None
|
|
else:
|
|
tr = tr._tr if (tr._tr is not None and not tr._tr._param) else None
|
|
|
|
|
|
def bind_all_attrs(Obj):
|
|
# bind content and constraints from typeref objects
|
|
# _cont, _root, _ext, _root_mand, _root_opt, _ext_ident, _ext_group, cont_tags
|
|
# _const_val, _const_sz, _const_tab, _const_tab_id, _const_tab_at, _const_cont, _const_alpha
|
|
bind_attrs(Obj, '_const_val')
|
|
bind_attrs(Obj, '_const_tab', '_const_tab_id', '_const_tab_at')
|
|
if Obj.TYPE == TYPE_INT:
|
|
bind_attrs(Obj, '_cont', '_cont_rev')
|
|
elif Obj.TYPE in (TYPE_REAL):
|
|
bind_attrs(Obj, '_cont', '_root', '_root_mand', '_root_opt')
|
|
bind_attrs(Obj, '_ext')
|
|
elif Obj.TYPE == TYPE_ENUM:
|
|
bind_attrs(Obj, '_cont', '_root', '_ext', '_cont_rev', '_const_ind')
|
|
elif Obj.TYPE == TYPE_BIT_STR:
|
|
bind_attrs(Obj, '_cont', '_cont_rev')
|
|
bind_attrs(Obj, '_const_sz')
|
|
bind_attrs(Obj, '_const_cont', '_const_cont_enc')
|
|
elif Obj.TYPE == TYPE_OCT_STR:
|
|
bind_attrs(Obj, '_const_sz')
|
|
bind_attrs(Obj, '_const_cont', '_const_cont_enc')
|
|
elif Obj.TYPE in TYPES_STRING:
|
|
bind_attrs(Obj, '_const_sz')
|
|
bind_attrs(Obj, '_const_alpha')
|
|
elif Obj.TYPE in (TYPE_SEQ_OF, TYPE_SET_OF):
|
|
bind_attrs(Obj, '_cont')
|
|
bind_attrs(Obj, '_const_sz')
|
|
elif Obj.TYPE == TYPE_CHOICE:
|
|
bind_attrs(Obj, '_cont', '_root', '_ext', '_const_ind')
|
|
elif Obj.TYPE in (TYPE_SEQ, TYPE_SET):
|
|
bind_attrs(Obj, '_cont', '_root', '_root_mand', '_root_opt')
|
|
bind_attrs(Obj, '_ext', '_ext_nest', '_ext_ident', '_ext_group', '_ext_group_obj')
|
|
elif Obj.TYPE == TYPE_CLASS:
|
|
bind_attrs(Obj, '_cont', '_root', '_root_mand', '_root_opt')
|
|
elif Obj.TYPE in (TYPE_EXT, TYPE_EMB_PDV, TYPE_CHAR_STR):
|
|
bind_attrs(Obj, '_cont', '_root', '_root_mand', '_root_opt')
|
|
bind_attrs(Obj, '_ext')
|
|
|
|
|
|
def build_classset_dict(Obj):
|
|
key = None
|
|
tr = get_typeref(Obj)
|
|
while not tr._cont:
|
|
tr = get_typeref(tr)
|
|
if tr is None:
|
|
break
|
|
if tr._cont is None:
|
|
# something is screwed somewhere
|
|
return
|
|
for Comp in tr._cont.values():
|
|
if Comp._uniq:
|
|
key = Comp._name
|
|
if key is None:
|
|
return
|
|
# check the key (UNIQUE) component
|
|
Obj._lut = {'__key__': key}
|
|
__build_classet_dict(Obj, key, Obj._val.root)
|
|
if Obj._val.ext:
|
|
__build_classet_dict(Obj, key, Obj._val.ext)
|
|
|
|
|
|
def __build_classet_dict(Obj, key, valset):
|
|
for val in valset:
|
|
if key in val:
|
|
keyval = val[key]
|
|
if keyval in Obj._lut:
|
|
# this is not as UNIQUE as one can think...
|
|
lutval = Obj._lut[keyval]
|
|
if lutval[0] == CLASET_UNIQ:
|
|
# switching to MULT
|
|
Obj._lut[keyval] = (CLASET_MULT, [lutval[1], val])
|
|
else:
|
|
# already defined as MULT
|
|
lutval[1].append(val)
|
|
else:
|
|
# this is the first (and hopefully UNIQUE) value
|
|
Obj._lut[keyval] = (CLASET_UNIQ, val)
|
|
|