mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-10-20 23:09:50 +03:00
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:
@@ -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 ---
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user