neels has uploaded this change for review.
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
---
M pySim/esim/saip/personalization.py
M tests/unittests/test_configurable_parameters.py
M tests/unittests/xo/test_configurable_parameters
3 files changed, 109 insertions(+), 118 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/64/43864/1
diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py
index a136aa8..e9ed709 100644
--- a/pySim/esim/saip/personalization.py
+++ b/pySim/esim/saip/personalization.py
@@ -420,68 +420,70 @@
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 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 @@
"""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):
diff --git a/tests/unittests/test_configurable_parameters.py b/tests/unittests/test_configurable_parameters.py
index 47f9eb8..0115640 100755
--- a/tests/unittests/test_configurable_parameters.py
+++ b/tests/unittests/test_configurable_parameters.py
@@ -148,7 +148,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 ---
diff --git a/tests/unittests/xo/test_configurable_parameters b/tests/unittests/xo/test_configurable_parameters
index 0946979..b2c9a5b 100644
--- a/tests/unittests/xo/test_configurable_parameters
+++ b/tests/unittests/xo/test_configurable_parameters
@@ -86,33 +86,33 @@
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 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
To view, visit change 43864. To unsubscribe, or for help writing mail filters, visit settings.