Add very basic profile for R-UIM (CDMA) cards

R-UIM (CDMA) cards are pretty much like the normal GSM SIM cards and
"speak" the same 2G APDU protocol, except that they have their own file
hierarchy under MF(3f00)/DF.CDMA(7f25).  They also have DF.TELECOM(7f10)
and even DF.GSM(7f20) with a limited subset of active EFs.  The content
of DF.CDMA is specified in 3GPP2 C.S0023-D.

This patch adds a very limited card profile for R-UIM, including auto-
detecion and a few EF definitions under DF.CDMA.  This may be useful
for people willing to explore or backup their R-UIMs.  To me this was
useful for playing with an R-UIM card from Skylink [1] - a Russian
MNO, which provided 450 MHz CDMA coverage until 2016.

[1] https://en.wikipedia.org/wiki/Sky_Link_(Russia)

Change-Id: Iacdebdbc514d1cd1910d173d81edd28578ec436a
diff --git a/pySim/cdma_ruim.py b/pySim/cdma_ruim.py
new file mode 100644
index 0000000..3fab558
--- /dev/null
+++ b/pySim/cdma_ruim.py
@@ -0,0 +1,193 @@
+# coding=utf-8
+"""R-UIM (Removable User Identity Module) card profile (see 3GPP2 C.S0023-D)
+
+(C) 2023 by Vadim Yanitskiy <fixeria@osmocom.org>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+import enum
+
+from pySim.utils import *
+from pySim.filesystem import *
+from pySim.profile import match_ruim
+from pySim.profile import CardProfile
+from pySim.ts_51_011 import CardProfileSIM
+from pySim.ts_51_011 import DF_TELECOM, DF_GSM
+from pySim.ts_51_011 import EF_ServiceTable
+from pySim.construct import *
+from construct import *
+
+
+# Mapping between CDMA Service Number and its description
+EF_CST_map = {
+    1 : 'CHV disable function',
+    2 : 'Abbreviated Dialing Numbers (ADN)',
+    3 : 'Fixed Dialing Numbers (FDN)',
+    4 : 'Short Message Storage (SMS)',
+    5 : 'HRPD',
+    6 : 'Enhanced Phone Book',
+    7 : 'Multi Media Domain (MMD)',
+    8 : 'SF_EUIMID-based EUIMID',
+    9 : 'MEID Support',
+    10 : 'Extension1',
+    11 : 'Extension2',
+    12 : 'SMS Parameters',
+    13 : 'Last Number Dialled (LND)',
+    14 : 'Service Category Program for BC-SMS',
+    15 : 'Messaging and 3GPD Extensions',
+    16 : 'Root Certificates',
+    17 : 'CDMA Home Service Provider Name',
+    18 : 'Service Dialing Numbers (SDN)',
+    19 : 'Extension3',
+    20 : '3GPD-SIP',
+    21 : 'WAP Browser',
+    22 : 'Java',
+    23 : 'Reserved for CDG',
+    24 : 'Reserved for CDG',
+    25 : 'Data Download via SMS Broadcast',
+    26 : 'Data Download via SMS-PP',
+    27 : 'Menu Selection',
+    28 : 'Call Control',
+    29 : 'Proactive R-UIM',
+    30 : 'AKA',
+    31 : 'IPv6',
+    32 : 'RFU',
+    33 : 'RFU',
+    34 : 'RFU',
+    35 : 'RFU',
+    36 : 'RFU',
+    37 : 'RFU',
+    38 : '3GPD-MIP',
+    39 : 'BCMCS',
+    40 : 'Multimedia Messaging Service (MMS)',
+    41 : 'Extension 8',
+    42 : 'MMS User Connectivity Parameters',
+    43 : 'Application Authentication',
+    44 : 'Group Identifier Level 1',
+    45 : 'Group Identifier Level 2',
+    46 : 'De-Personalization Control Keys',
+    47 : 'Cooperative Network List',
+}
+
+
+######################################################################
+# DF.CDMA
+######################################################################
+
+class EF_SPN(TransparentEF):
+    '''3.4.31 CDMA Home Service Provider Name'''
+
+    _test_de_encode = [
+        ( "010801536b796c696e6b204e57ffffffffffffffffffffffffffffffffffffffffffff",
+          { 'rfu0' : 0, 'show_in_hsa' : True, 'rfu2' : 0,
+            'char_encoding' : 8, 'lang_ind' : 1, 'spn' : 'Skylink NW' } ),
+    ]
+
+    def __init__(self, fid='6f41', sfid=None, name='EF.SPN',
+                 desc='Service Provider Name', size=(35, 35), **kwargs):
+        super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
+        self._construct = BitStruct(
+            # Byte 1: Display Condition
+            'rfu1'/BitsRFU(7),
+            'show_in_hsa'/Flag,
+            # Byte 2: Character Encoding
+            'rfu2'/BitsRFU(3),
+            'char_encoding'/BitsInteger(5), # see C.R1001-G
+            # Byte 3: Language Indicator
+            'lang_ind'/BitsInteger(8), # see C.R1001-G
+            # Bytes 4-35: Service Provider Name
+            'spn'/Bytewise(GsmString(32))
+        )
+
+class EF_AD(TransparentEF):
+    '''3.4.33 Administrative Data'''
+
+    _test_de_encode = [
+        ( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
+    ]
+
+    class OP_MODE(enum.IntEnum):
+        normal = 0x00
+        type_approval = 0x80
+        normal_and_specific_facilities = 0x01
+        type_approval_and_specific_facilities = 0x81
+        maintenance_off_line = 0x02
+        cell_test = 0x04
+
+    def __init__(self, fid='6f43', sfid=None, name='EF.AD',
+                 desc='Service Provider Name', size=(3, None), **kwargs):
+        super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
+        self._construct = Struct(
+            # Byte 1: Display Condition
+            'ms_operation_mode'/Enum(Byte, self.OP_MODE),
+            # Bytes 2-3: Additional information
+            'additional_info'/Bytes(2),
+            # Bytes 4..: RFU
+            'rfu'/GreedyBytesRFU,
+        )
+
+
+class EF_SMS(LinFixedEF):
+    '''3.4.27 Short Messages'''
+    def __init__(self, fid='6f3c', sfid=None, name='EF.SMS', desc='Short messages', **kwargs):
+        super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(2, 255), **kwargs)
+        self._construct = Struct(
+            # Byte 1: Status
+            'status'/BitStruct(
+                'rfu87'/BitsRFU(2),
+                'protection'/Flag,
+                'rfu54'/BitsRFU(2),
+                'status'/FlagsEnum(BitsInteger(2), read=0, to_be_read=1, sent=2, to_be_sent=3),
+                'used'/Flag,
+            ),
+            # Byte 2: Length
+            'length'/Int8ub,
+            # Bytes 3..: SMS Transport Layer Message
+            'tpdu'/Bytes(lambda ctx: ctx.length if ctx.status.used else 0),
+        )
+
+
+class DF_CDMA(CardDF):
+    def __init__(self):
+        super().__init__(fid='7f25', name='DF.CDMA',
+                         desc='CDMA related files (3GPP2 C.S0023-D)')
+        files = [
+            # TODO: lots of other files
+            EF_ServiceTable('6f32', None, 'EF.CST',
+                            'CDMA Service Table', table=EF_CST_map, size=(5, 16)),
+            EF_SPN(),
+            EF_AD(),
+            EF_SMS(),
+        ]
+        self.add_files(files)
+
+
+class CardProfileRUIM(CardProfile):
+    '''R-UIM card profile as per 3GPP2 C.S0023-D'''
+
+    ORDER = 2
+
+    def __init__(self):
+        super().__init__('R-UIM', desc='CDMA R-UIM Card', cla="a0",
+                         sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM(), DF_CDMA()])
+
+    @staticmethod
+    def decode_select_response(resp_hex: str) -> object:
+        # TODO: Response parameters/data in case of DF_CDMA (section 2.6)
+        return CardProfileSIM.decode_select_response(resp_hex)
+
+    @staticmethod
+    def match_with_card(scc: SimCardCommands) -> bool:
+        return match_ruim(scc)
diff --git a/pySim/profile.py b/pySim/profile.py
index c535bac..e464e1f 100644
--- a/pySim/profile.py
+++ b/pySim/profile.py
@@ -26,9 +26,12 @@
 from pySim.utils import all_subclasses
 import abc
 import operator
+from typing import List
 
 
-def _mf_select_test(scc: SimCardCommands, cla_byte: str, sel_ctrl: str) -> bool:
+def _mf_select_test(scc: SimCardCommands,
+                    cla_byte: str, sel_ctrl: str,
+                    fids: List[str]) -> bool:
     cla_byte_bak = scc.cla_byte
     sel_ctrl_bak = scc.sel_ctrl
     scc.reset_card()
@@ -37,7 +40,8 @@
     scc.sel_ctrl = sel_ctrl
     rc = True
     try:
-        scc.select_file('3f00')
+        for fid in fids:
+            scc.select_file(fid)
     except:
         rc = False
 
@@ -51,7 +55,7 @@
     """ Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
     card is considered a UICC card.
     """
-    return _mf_select_test(scc, "00", "0004")
+    return _mf_select_test(scc, "00", "0004", ["3f00"])
 
 
 def match_sim(scc: SimCardCommands) -> bool:
@@ -59,7 +63,14 @@
     is also a simcard. This will be the case for most UICC cards, but there may
     also be plain UICC cards without 2G support as well.
     """
-    return _mf_select_test(scc, "a0", "0000")
+    return _mf_select_test(scc, "a0", "0000", ["3f00"])
+
+
+def match_ruim(scc: SimCardCommands) -> bool:
+    """ Try to access MF/DF.CDMA via 2G APDUs (3GPP TS 11.11), if this works,
+    the card is considered an R-UIM card for CDMA.
+    """
+    return _mf_select_test(scc, "a0", "0000", ["3f00", "7f25"])
 
 
 class CardProfile:
diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py
index 222bdaf..e9f0945 100644
--- a/pySim/ts_51_011.py
+++ b/pySim/ts_51_011.py
@@ -1200,7 +1200,7 @@
 
 class CardProfileSIM(CardProfile):
 
-    ORDER = 2
+    ORDER = 3
 
     def __init__(self):
         sw = {