ts_102_221: Proper parsing of FCP using pySim.tlv instead of pytlv

pytlv is a nightmare of shortcomings, let's abandon it in favor of
our own meanwhile-created pySim.tlv.  This has the added benefit
that unknown tags finally no longer raise exceptions.

Change-Id: Ic8e0e0ddf915949670d620630d4ceb02a9116471
Closes: OS#5414
diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py
index da4eb93..24c9589 100644
--- a/pySim/ts_102_221.py
+++ b/pySim/ts_102_221.py
@@ -17,7 +17,6 @@
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 """
 
-from pytlv.TLV import *
 from construct import *
 from pySim.construct import *
 from pySim.utils import *
@@ -78,131 +77,159 @@
     CardCommand('RESIZE FILE',              0xD4, ['8X', 'CX']),
 ])
 
+# ETSI TS 102 221 11.1.1.4.2
+class FileSize(BER_TLV_IE, tag=0x80):
+    _construct = GreedyInteger()
 
-FCP_TLV_MAP = {
-    '82': 'file_descriptor',
-    '83': 'file_identifier',
-    '84': 'df_name',
-    'A5': 'proprietary_info',
-    '8A': 'life_cycle_status_int',
-    '8B': 'security_attrib_ref_expanded',
-    '8C': 'security_attrib_compact',
-    'AB': 'security_attrib_espanded',
-    'C6': 'pin_status_template_do',
-    '80': 'file_size',
-    '81': 'total_file_size',
-    '88': 'short_file_id',
-}
-
-# ETSI TS 102 221 11.1.1.4.6
-FCP_Proprietary_TLV_MAP = {
-    '80': 'uicc_characteristics',
-    '81': 'application_power_consumption',
-    '82': 'minimum_app_clock_freq',
-    '83': 'available_memory',
-    '84': 'file_details',
-    '85': 'reserved_file_size',
-    '86': 'maximum_file_size',
-    '87': 'suported_system_commands',
-    '88': 'specific_uicc_env_cond',
-    '89': 'p2p_cat_secured_apdu',
-    # Additional private TLV objects (bits b7 and b8 of the first byte of the tag set to '1')
-}
+# ETSI TS 102 221 11.1.1.4.2
+class TotalFileSize(BER_TLV_IE, tag=0x81):
+    _construct = GreedyInteger()
 
 # ETSI TS 102 221 11.1.1.4.3
+class FileDescriptor(BER_TLV_IE, tag=0x82):
+    def _from_bytes(self, in_bin: bytes):
+        out = {}
+        ft_dict = {
+            0: 'working_ef',
+            1: 'internal_ef',
+            7: 'df'
+        }
+        fs_dict = {
+            0: 'no_info_given',
+            1: 'transparent',
+            2: 'linear_fixed',
+            6: 'cyclic',
+            0x39: 'ber_tlv',
+        }
+        fdb = in_bin[0]
+        ftype = (fdb >> 3) & 7
+        if fdb & 0xbf == 0x39:
+            fstruct = 0x39
+        else:
+            fstruct = fdb & 7
+        out['shareable'] = True if fdb & 0x40 else False
+        out['file_type'] = ft_dict[ftype] if ftype in ft_dict else ftype
+        out['structure'] = fs_dict[fstruct] if fstruct in fs_dict else fstruct
+        if len(in_bin) >= 5:
+            out['record_len'] = int.from_bytes(in_bin[2:4], 'big')
+            out['num_of_rec'] = int.from_bytes(in_bin[4:5], 'big')
+        self.decoded = out
+        return self.decoded
 
+# ETSI TS 102 221 11.1.1.4.4
+class FileIdentifier(BER_TLV_IE, tag=0x83):
+    _construct = GreedyBytes
 
-def interpret_file_descriptor(in_hex):
-    in_bin = h2b(in_hex)
-    out = {}
-    ft_dict = {
-        0: 'working_ef',
-        1: 'internal_ef',
-        7: 'df'
-    }
-    fs_dict = {
-        0: 'no_info_given',
-        1: 'transparent',
-        2: 'linear_fixed',
-        6: 'cyclic',
-        0x39: 'ber_tlv',
-    }
-    fdb = in_bin[0]
-    ftype = (fdb >> 3) & 7
-    if fdb & 0xbf == 0x39:
-        fstruct = 0x39
-    else:
-        fstruct = fdb & 7
-    out['shareable'] = True if fdb & 0x40 else False
-    out['file_type'] = ft_dict[ftype] if ftype in ft_dict else ftype
-    out['structure'] = fs_dict[fstruct] if fstruct in fs_dict else fstruct
-    if len(in_bin) >= 5:
-        out['record_len'] = int.from_bytes(in_bin[2:4], 'big')
-        out['num_of_rec'] = int.from_bytes(in_bin[4:5], 'big')
-    return out
+# ETSI TS 102 221 11.1.1.4.5
+class DfName(BER_TLV_IE, tag=0x84):
+    _construct = GreedyBytes
+
+# ETSI TS 102 221 11.1.1.4.6.1
+class UiccCharacteristics(BER_TLV_IE, tag=0x80):
+    _construct = GreedyBytes
+
+# ETSI TS 102 221 11.1.1.4.6.2
+class ApplicationPowerConsumption(BER_TLV_IE, tag=0x81):
+    _construct = Struct('voltage_class'/Int8ub,
+                        'power_consumption_ma'/Int8ub,
+                        'reference_freq_100k'/Int8ub)
+
+# ETSI TS 102 221 11.1.1.4.6.3
+class MinApplicationClockFrequency(BER_TLV_IE, tag=0x82):
+    _construct = Int8ub
+
+# ETSI TS 102 221 11.1.1.4.6.4
+class AvailableMemory(BER_TLV_IE, tag=0x83):
+    _construct = GreedyInteger()
+
+# ETSI TS 102 221 11.1.1.4.6.5
+class FileDetails(BER_TLV_IE, tag=0x84):
+    _construct = FlagsEnum(Byte, der_coding_only=1)
+
+# ETSI TS 102 221 11.1.1.4.6.6
+class ReservedFileSize(BER_TLV_IE, tag=0x85):
+    _construct = GreedyInteger()
+
+# ETSI TS 102 221 11.1.1.4.6.7
+class MaximumFileSize(BER_TLV_IE, tag=0x86):
+    _construct = GreedyInteger()
+
+# ETSI TS 102 221 11.1.1.4.6.8
+class SupportedFilesystemCommands(BER_TLV_IE, tag=0x87):
+    _construct = FlagsEnum(Byte, terminal_capability=1)
+
+# ETSI TS 102 221 11.1.1.4.6.9
+class SpecificUiccEnvironmentConditions(BER_TLV_IE, tag=0x88):
+    _construct = BitStruct('rfu'/BitsRFU(4),
+                           'high_humidity_supported'/Flag,
+                           'temperature_class'/Enum(BitsInteger(3), standard=0, class_A=1, class_B=2, class_C=3))
+
+# ETSI TS 102 221 11.1.1.4.6.10
+class Platform2PlatformCatSecuredApdu(BER_TLV_IE, tag=0x89):
+    _construct = GreedyBytes
+
+# ETSI TS 102 221 11.1.1.4.6.0
+class ProprietaryInformation(BER_TLV_IE, tag=0xA5,
+                             nested=[UiccCharacteristics, ApplicationPowerConsumption,
+                                     MinApplicationClockFrequency, AvailableMemory,
+                                     FileDetails, ReservedFileSize, MaximumFileSize,
+                                     SupportedFilesystemCommands, SpecificUiccEnvironmentConditions]):
+    pass
+
+# ETSI TS 102 221 11.1.1.4.7.1
+class SecurityAttribCompact(BER_TLV_IE, tag=0x8c):
+    _construct = GreedyBytes
+
+# ETSI TS 102 221 11.1.1.4.7.2
+class SecurityAttribExpanded(BER_TLV_IE, tag=0xab):
+    _construct = GreedyBytes
+
+# ETSI TS 102 221 11.1.1.4.7.3
+class SecurityAttribReferenced(BER_TLV_IE, tag=0x8b):
+    # TODO: longer format with SEID
+    _construct = Struct('ef_arr_file_id'/Bytes(2), 'ef_arr_record_nr'/Int8ub)
+
+# ETSI TS 102 221 11.1.1.4.8
+class ShortFileIdentifier(BER_TLV_IE, tag=0x88):
+    _construct = Byte
 
 # ETSI TS 102 221 11.1.1.4.9
+class LifeCycleStatusInteger(BER_TLV_IE, tag=0x8A):
+    def _from_bytes(self, do: bytes):
+        lcsi = int.from_bytes(do, 'big')
+        if lcsi == 0x00:
+            ret = 'no_information'
+        elif lcsi == 0x01:
+            ret = 'creation'
+        elif lcsi == 0x03:
+            ret = 'initialization'
+        elif lcsi & 0x05 == 0x05:
+            ret = 'operational_activated'
+        elif lcsi & 0x05 == 0x04:
+            ret = 'operational_deactivated'
+        elif lcsi & 0xc0 == 0xc0:
+            ret = 'termination'
+        else:
+            ret = lcsi
+        self.decoded = ret
+        return self.decoded
 
+# ETSI TS 102 221 11.1.1.4.9
+class PS_DO(BER_TLV_IE, tag=0x90):
+    _construct = GreedyBytes
+class UsageQualifier_DO(BER_TLV_IE, tag=0x95):
+    _construct = GreedyBytes
+class KeyReference(BER_TLV_IE, tag=0x83):
+    _construct = Byte
+class PinStatusTemplate_DO(BER_TLV_IE, tag=0xC6, nested=[PS_DO, UsageQualifier_DO, KeyReference]):
+    pass
 
-def interpret_life_cycle_sts_int(in_hex):
-    lcsi = int(in_hex, 16)
-    if lcsi == 0x00:
-        return 'no_information'
-    elif lcsi == 0x01:
-        return 'creation'
-    elif lcsi == 0x03:
-        return 'initialization'
-    elif lcsi & 0x05 == 0x05:
-        return 'operational_activated'
-    elif lcsi & 0x05 == 0x04:
-        return 'operational_deactivated'
-    elif lcsi & 0xc0 == 0xc0:
-        return 'termination'
-    else:
-        return in_hex
-
-
-# ETSI TS 102 221 11.1.1.4.10
-FCP_Pin_Status_TLV_MAP = {
-    '90': 'ps_do',
-    '95': 'usage_qualifier',
-    '83': 'key_reference',
-}
-
-
-def interpret_ps_templ_do(in_hex):
-    # cannot use the 'TLV' parser due to repeating tags
-    #psdo_tlv = TLV(FCP_Pin_Status_TLV_MAP)
-    # return psdo_tlv.parse(in_hex)
-    return in_hex
-
-
-# 'interpreter' functions for each tag
-FCP_interpreter_map = {
-    '80': lambda x: int(x, 16),
-    '82': interpret_file_descriptor,
-    '8A': interpret_life_cycle_sts_int,
-    'C6': interpret_ps_templ_do,
-}
-
-FCP_prorietary_interpreter_map = {
-    '83': lambda x: int(x, 16),
-}
-
-# pytlv unfortunately doesn't have a setting using which we can make it
-# accept unknown tags.  It also doesn't raise a specific exception type but
-# just the generic ValueError, so we cannot ignore those either.  Instead,
-# we insert a dict entry for every possible proprietary tag permitted
-
-
-def fixup_fcp_proprietary_tlv_map(tlv_map):
-    if 'D0' in tlv_map:
-        return
-    for i in range(0xc0, 0xff):
-        i_hex = i2h([i]).upper()
-        tlv_map[i_hex] = 'proprietary_' + i_hex
-    # Other non-standard TLV objects found on some cards
-    tlv_map['9B'] = 'target_ef'  # for sysmoUSIM-SJS1
+class FcpTemplate(BER_TLV_IE, tag=0x62, nested=[FileSize, TotalFileSize, FileDescriptor, FileIdentifier,
+                                                DfName, ProprietaryInformation, SecurityAttribCompact,
+                                                SecurityAttribExpanded, SecurityAttribReferenced,
+                                                ShortFileIdentifier, LifeCycleStatusInteger,
+                                                PinStatusTemplate_DO]):
+    pass
 
 
 def tlv_key_replace(inmap, indata):
@@ -759,23 +786,10 @@
     @staticmethod
     def decode_select_response(resp_hex: str) -> object:
         """ETSI TS 102 221 Section 11.1.1.3"""
-        fixup_fcp_proprietary_tlv_map(FCP_Proprietary_TLV_MAP)
-        resp_hex = resp_hex.upper()
-        # outer layer
-        fcp_base_tlv = TLV(['62'])
-        fcp_base = fcp_base_tlv.parse(resp_hex)
-        # actual FCP
-        fcp_tlv = TLV(FCP_TLV_MAP)
-        fcp = fcp_tlv.parse(fcp_base['62'])
-        # further decode the proprietary information
-        if 'A5' in fcp:
-            prop_tlv = TLV(FCP_Proprietary_TLV_MAP)
-            prop = prop_tlv.parse(fcp['A5'])
-            fcp['A5'] = tlv_val_interpret(FCP_prorietary_interpreter_map, prop)
-            fcp['A5'] = tlv_key_replace(FCP_Proprietary_TLV_MAP, fcp['A5'])
-        # finally make sure we get human-readable keys in the output dict
-        r = tlv_val_interpret(FCP_interpreter_map, fcp)
-        return tlv_key_replace(FCP_TLV_MAP, r)
+        t = FcpTemplate()
+        t.from_tlv(h2b(resp_hex))
+        d = t.to_dict()
+        return flatten_dict_lists(d['fcp_template'])
 
     @staticmethod
     def match_with_card(scc: SimCardCommands) -> bool: