neels has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/43864?usp=email )
Change subject: personalization: EnumParam: implement as value_map, not enum.IntEnum ......................................................................
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