personalization: EnumParam: implement as value_map, not enum.IntEnum

Because of (1) and (2) below, using enum.IntEnum is not a good choice
for EnumParam. Use a plain dict as value map.

EnumParam's idea is to be a mapping
    human-readable-value-for-UI -> PES-value
and with enum.IntEnum we...

(1) ...we impose python namespace rules onto
human-readable-value-for-UI: "My Value" is not allowed, only "my_value".
By using enum.IntEnum, we make it impossible to use human readable
values like:
- "SUCI-on" (dash not allowed in python identifier)
- "True" (python keyword not allowed)
- "2" (numeric constant cannot be a python identifier)

(2) ...we limit PES-values to int. The mapping should support any type
that the given PES attribute needs (int, bool, bytes, ...).

Future: upcoming ConfigurableParameters depend on this patch:
- MncLen (labels "2" and "3")
- EuiccMandatoryServiceParam (values True and False)
- EfUstServiceParam like SuciInUsim (labels "SUCI-in-UE" and
  "SUCI-in-USIM")

History: this is near the first version of EnumParam, before it was
modified by fixeria to use enum.IntEnum, as requested by CR. I realize
now that enum.IntEnum will not work for upcoming eSIM parameters.

Change-Id: I690ceccf0ec7ef7067bcaa5cec1303cdaf0f78a4
Jenkins: skip-card-test
This commit is contained in:
Neels Hofmeyr
2026-06-26 20:52:34 +02:00
parent 9aa8934da2
commit 43a9f65779
3 changed files with 109 additions and 118 deletions
+86 -96
View File
@@ -420,68 +420,70 @@ class BinaryParam(ConfigurableParameter):
class EnumParam(ConfigurableParameter):
"""ConfigurableParameter for named integer enumeration values.
"""ConfigurableParameter for named value enumerations.
Subclasses must define a nested enum.IntEnum named 'Values' listing all valid names and their
integer codes. apply_val() and get_values_from_pes() are not implemented here and this must
be inherited from another mixin."""
class Values(enum.IntEnum):
pass # subclasses override this
Subclasses define an own value_map, and implement their own apply_val() and get_values_from_pes().
"""
value_map = {
# For example:
#'Meaningful label for value 23': 0x23,
# Where 0x23 is a valid value to use for apply_val(), of any valid type.
}
_value_map_reverse = None
@classmethod
def validate_val(cls, val) -> int:
if isinstance(val, int):
try:
return int(cls.Values(val))
except ValueError:
pass
elif isinstance(val, str):
member = cls.map_name_to_val(val, strict=False)
if member is not None:
return member
def validate_val(cls, val):
orig_val = val
enum_val = None
if isinstance(val, str):
enum_name = val
enum_val = cls.map_name_to_val(enum_name)
valid = ', '.join(m.name for m in cls.Values)
raise ValueError(f"{cls.get_name()}: invalid argument: {val!r}. Valid arguments are: {valid}")
# if the str is not one of the known value_map.keys(), is it maybe one of value_map.keys()?
if enum_val is None and val in cls.value_map.values():
enum_val = val
if enum_val not in cls.value_map.values():
raise ValueError(f"{cls.get_name()}: invalid argument: {orig_val!r}. Valid arguments are:"
f" {', '.join(cls.value_map.keys())}")
return enum_val
@classmethod
def map_name_to_val(cls, name: str, strict=True) -> int:
"""Return the integer value for a given enum member name. Performs an exact match first,
then falls back to fuzzy matching (case-insensitive, punctuation-insensitive)."""
try:
return int(cls.Values[name])
except KeyError:
pass
def map_name_to_val(cls, name:str, strict=True):
val = cls.value_map.get(name)
if val is not None:
return val
clean = cls.clean_name_str(name)
for member in cls.Values:
if cls.clean_name_str(member.name) == clean:
return int(member)
clean_name = cls.clean_name_str(name)
for k, v in cls.value_map.items():
if clean_name == cls.clean_name_str(k):
return v
if strict:
valid = ', '.join(m.name for m in cls.Values)
raise ValueError(f"{cls.get_name()}: {name!r} is not a known value. Known values are: {valid}")
raise ValueError(f"Problem in {cls.get_name()}: {name!r} is not a known value."
f" Known values are: {cls.value_map.keys()!r}")
return None
@classmethod
def map_val_to_name(cls, val, strict=False) -> str:
"""Return the enum member name for a given integer value."""
try:
return cls.Values(val).name
except ValueError:
if strict:
raise ValueError(f"{cls.get_name()}: {val!r} ({type(val).__name__}) is not a known value.")
return None
if cls._value_map_reverse is None:
cls._value_map_reverse = dict((v, k) for k, v in cls.value_map.items())
name = cls._value_map_reverse.get(val)
if name:
return name
if strict:
raise ValueError(f"Problem in {cls.get_name()}: {val!r} ({type(val)}) is not a known value."
f" Known values are: {cls.value_map.values()!r}")
return None
@classmethod
def name_normalize(cls, name: str) -> str:
"""Map a (possibly fuzzy) name to its canonical enum member name."""
return cls.Values(cls.map_name_to_val(name)).name
def name_normalize(cls, name:str) -> str:
return cls.map_val_to_name(cls.map_name_to_val(name))
@classmethod
def clean_name_str(cls, val: str) -> str:
"""Strip punctuation and case for fuzzy name comparison.
Treats hyphens and underscores as equivalent (both removed)."""
def clean_name_str(cls, val):
return re.sub('[^0-9A-Za-z]', '', val).lower()
@@ -664,69 +666,57 @@ class SmspTpScAddr(ConfigurableParameter):
class MncLen(EnumParam):
"""MNC length. Sets only the MNC length field in EF.AD (Administrative Data).
Accepted values: integer 2 or 3, digit strings '2' or '3', or enum names 'MNC2'/'MNC3'.
"""
"""MNC length. Must be either 2 or 3. Sets only the MNC length field in EF-AD (Administrative Data)."""
name = 'MNC-LEN'
example_input = '2'
value_map = { '2': 2, '3': 3 }
default_source = param_source.ConstantSource
class Values(enum.IntEnum):
MNC2 = 2
MNC3 = 3
example_input = '2'
@classmethod
def validate_val(cls, val):
if isinstance(val, str) and val.isdigit():
val = int(val)
return super().validate_val(val)
@classmethod
def _get_f_ad(cls, pe: ProfileElement):
if not hasattr(pe, 'files'):
return None
f_ad = pe.files.get('ef-ad', None)
if f_ad and f_ad.body:
return f_ad
return None
@classmethod
def _decode_f_ad(cls, f_ad):
try:
ef_ad_dec = EF_AD().decode_bin(f_ad.body)
except StreamError:
return None
if 'mnc_len' not in ef_ad_dec:
return None
return ef_ad_dec
@classmethod
def apply_val(cls, pes: ProfileElementSequence, val: int):
def apply_val(cls, pes: ProfileElementSequence, val):
"""val must be an int: either 2 or 3"""
for pe in pes.get_pes_for_type('usim'):
f_ad = cls._get_f_ad(pe)
if f_ad is None:
if not hasattr(pe, 'files'):
continue
f_ad = pe.files.get('ef-ad')
if not f_ad:
continue
# decode existing values
ef_ad_dec = cls._decode_f_ad(f_ad)
if ef_ad_dec is None:
if not f_ad.body:
continue
try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue
if 'mnc_len' not in ef_ad_dec:
continue
# change mnc_len
ef_ad_dec['mnc_len'] = val
# re-encode into the File body
f_ad.body = EF_AD().encode_bin(ef_ad_dec)
f_ad.body = ef_ad.encode_bin(ef_ad_dec)
pe.file2pe(f_ad)
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
for pe in pes.get_pes_for_type('usim'):
f_ad = cls._get_f_ad(pe)
if not hasattr(pe, 'files'):
continue
f_ad = pe.files.get('ef-ad', None)
if f_ad is None:
continue
ef_ad_dec = cls._decode_f_ad(f_ad)
if ef_ad_dec is None:
try:
ef_ad = EF_AD()
ef_ad_dec = ef_ad.decode_bin(f_ad.body)
except StreamError:
continue
mnc_len = ef_ad_dec.get('mnc_len')
yield { cls.name: str(mnc_len) }
mnc_len = ef_ad_dec.get('mnc_len', None)
if mnc_len is None:
continue
yield { cls.name: cls.map_val_to_name(int(mnc_len)) }
class SdKey(BinaryParam):
@@ -1100,17 +1090,17 @@ class AlgorithmID(EnumParam, AlgoConfig):
"""use validate_val() from EnumParam, and apply_val() from AlgoConfig.
In get_values_from_pes(), return enum value names, not raw values."""
name = "Algorithm"
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
value_map = {
"Milenage" : 1,
"TUAK" : 2,
"usim-test" : 3,
}
algo_config_key = 'algorithmID'
example_input = "Milenage"
default_source = param_source.ConstantSource
# as in pySim/esim/asn1/saip/PE_Definitions-3.3.1.asn
class Values(enum.IntEnum):
Milenage = 1
TUAK = 2
usim_test = 3 # input 'usim-test' also accepted via fuzzy matching
# EnumParam.validate_val() returns the int values from Values
# EnumParam.validate_val() returns the int values from value_map
@classmethod
def get_values_from_pes(cls, pes: ProfileElementSequence):
@@ -148,7 +148,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID,
val='usim-test',
expect_clean_val=3,
expect_val='usim_test'),
expect_val='usim-test'),
Paramtest(param_cls=p13n.AlgorithmID,
val=1,
@@ -161,7 +161,7 @@ class ConfigurableParameterTest(unittest.TestCase):
Paramtest(param_cls=p13n.AlgorithmID,
val=3,
expect_clean_val=3,
expect_val='usim_test'),
expect_val='usim-test'),
Paramtest(param_cls=p13n.K,
val='01020304050607080910111213141516',
@@ -559,7 +559,7 @@ class TestEnumParam(unittest.TestCase):
def test_validate_by_name_exact(self):
self.assertEqual(p13n.AlgorithmID.validate_val('Milenage'), 1)
self.assertEqual(p13n.AlgorithmID.validate_val('TUAK'), 2)
self.assertEqual(p13n.AlgorithmID.validate_val('usim_test'), 3)
self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3)
def test_validate_by_int(self):
self.assertEqual(p13n.AlgorithmID.validate_val(1), 1)
@@ -572,7 +572,6 @@ class TestEnumParam(unittest.TestCase):
self.assertEqual(p13n.AlgorithmID.validate_val('tuak'), 2)
def test_validate_fuzzy_hyphen_underscore(self):
# 'usim-test' has a hyphen; enum member is 'usim_test' — must fuzzy-match
self.assertEqual(p13n.AlgorithmID.validate_val('usim-test'), 3)
def test_validate_invalid_name(self):
@@ -609,7 +608,7 @@ class TestEnumParam(unittest.TestCase):
def test_map_val_known(self):
self.assertEqual(p13n.AlgorithmID.map_val_to_name(1), 'Milenage')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(2), 'TUAK')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim_test')
self.assertEqual(p13n.AlgorithmID.map_val_to_name(3), 'usim-test')
def test_map_val_unknown_nonstrict(self):
self.assertIsNone(p13n.AlgorithmID.map_val_to_name(99))
@@ -623,7 +622,9 @@ class TestEnumParam(unittest.TestCase):
def test_name_normalize(self):
self.assertEqual(p13n.AlgorithmID.name_normalize('Milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('milenage'), 'Milenage')
self.assertEqual(p13n.AlgorithmID.name_normalize('usim-test'), 'usim_test')
self.assertEqual(p13n.AlgorithmID.name_normalize('usimtest'), 'usim-test')
self.assertEqual(p13n.AlgorithmID.name_normalize('usim_test'), 'usim-test')
self.assertEqual(p13n.AlgorithmID.name_normalize('USIM Test'), 'usim-test')
# --- clean_name_str ---
+16 -16
View File
@@ -86,33 +86,33 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der Adm1(val=123456:int)
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val='Milenage':str)
clean_val=1:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'Milenage'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val='TUAK':str)
clean_val=2:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'TUAK'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val='usim-test':str)
clean_val=3:int
previous value: ['usim_test']
read_back_val={'Algorithm': 'usim_test'}:{str}
previous value: ['usim-test']
read_back_val={'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val=1:int)
clean_val=1:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'Milenage'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val=2:int)
clean_val=2:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'TUAK'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val=3:int)
clean_val=3:int
previous value: ['usim_test']
read_back_val={'Algorithm': 'usim_test'}:{str}
previous value: ['usim-test']
read_back_val={'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.1A_NoBERTLV.der K(val='01020304050607080910111213141516':str)
clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes
@@ -961,33 +961,33 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der Adm1(val=123456:int)
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val='Milenage':str)
clean_val=1:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'Milenage'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val='TUAK':str)
clean_val=2:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'TUAK'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val='usim-test':str)
clean_val=3:int
previous value: ['usim_test']
read_back_val={'Algorithm': 'usim_test'}:{str}
previous value: ['usim-test']
read_back_val={'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val=1:int)
clean_val=1:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'Milenage'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val=2:int)
clean_val=2:int
previous value: ['usim_test']
previous value: ['usim-test']
read_back_val={'Algorithm': 'TUAK'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val=3:int)
clean_val=3:int
previous value: ['usim_test']
read_back_val={'Algorithm': 'usim_test'}:{str}
previous value: ['usim-test']
read_back_val={'Algorithm': 'usim-test'}:{str}
ok: TS48v5_SAIP2.3_BERTLV_SUCI.der K(val='01020304050607080910111213141516':str)
clean_val=b'\x01\x02\x03\x04\x05\x06\x07\x08\t\x10\x11\x12\x13\x14\x15\x16':bytes