diff --git a/pySim/esim/saip/personalization.py b/pySim/esim/saip/personalization.py index 6f88d52f..ede3406e 100644 --- a/pySim/esim/saip/personalization.py +++ b/pySim/esim/saip/personalization.py @@ -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() @@ -663,69 +665,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): @@ -1099,17 +1089,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): diff --git a/tests/unittests/test_configurable_parameters.py b/tests/unittests/test_configurable_parameters.py index aef2372c..950228f4 100755 --- a/tests/unittests/test_configurable_parameters.py +++ b/tests/unittests/test_configurable_parameters.py @@ -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', @@ -558,7 +558,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) @@ -571,7 +571,7 @@ 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 + # '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): @@ -608,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)) @@ -622,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 --- diff --git a/tests/unittests/xo/test_configurable_parameters b/tests/unittests/xo/test_configurable_parameters index 921121fe..17afd271 100644 --- a/tests/unittests/xo/test_configurable_parameters +++ b/tests/unittests/xo/test_configurable_parameters @@ -77,7 +77,7 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 'TUAK':str) ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 'usim-test':str) clean_val= 3:int - read_back_val= {'Algorithm': 'usim_test'}:{str} + read_back_val= {'Algorithm': 'usim-test'}:{str} ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 1:int) clean_val= 1:int @@ -89,7 +89,7 @@ ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 2:int) ok: TS48v5_SAIP2.1A_NoBERTLV.der AlgorithmID(val= 3:int) clean_val= 3:int - read_back_val= {'Algorithm': 'usim_test'}:{str} + 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 @@ -777,7 +777,7 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 'TUAK':str) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 'usim-test':str) clean_val= 3:int - read_back_val= {'Algorithm': 'usim_test'}:{str} + read_back_val= {'Algorithm': 'usim-test'}:{str} ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 1:int) clean_val= 1:int @@ -789,7 +789,7 @@ ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 2:int) ok: TS48v5_SAIP2.3_BERTLV_SUCI.der AlgorithmID(val= 3:int) clean_val= 3:int - read_back_val= {'Algorithm': 'usim_test'}:{str} + 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