Introduce concept of CardProfileAddon

We have a strict "one CardProfile per card" rule.  For a modern UICC
without legacy SIM support, that works great, as all applications
have AID and ADF and can hence be enumerated/detected that way.

However, in reality there are mostly UICC that have legacy SIM, GSM-R
or even CDMA support, all of which are not proper UICC applications
for historical reasons.

So instead of having hard-coded hacks in various places, let's introduce
the new concept of a CardProfileAddon.  Every profile can have any
number of those.  When building up the RuntimeState, we iterate over the
CardProfile addons, and probe which of those are actually on the card.
For those discovered, we add their files to the filesystem hierarchy.

Change-Id: I5866590b6d48f85eb889c9b1b8ab27936d2378b9
diff --git a/pySim/cdma_ruim.py b/pySim/cdma_ruim.py
index 8b66490..b254403 100644
--- a/pySim/cdma_ruim.py
+++ b/pySim/cdma_ruim.py
@@ -22,7 +22,7 @@
 from pySim.utils import *
 from pySim.filesystem import *
 from pySim.profile import match_ruim
-from pySim.profile import CardProfile
+from pySim.profile import CardProfile, CardProfileAddon
 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
@@ -191,3 +191,14 @@
     @staticmethod
     def match_with_card(scc: SimCardCommands) -> bool:
         return match_ruim(scc)
+
+class AddonRUIM(CardProfileAddon):
+    """An Addon that can be found on on a combined SIM + RUIM or UICC + RUIM to support CDMA."""
+    def __init__(self):
+        files = [
+            DF_CDMA()
+        ]
+        super().__init__('RUIM', desc='CDMA RUIM', files_in_mf=files)
+
+    def probe(self, card: 'CardBase') -> bool:
+        return card.file_exists(self.files_in_mf[0].fid)
diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index 22ff60d..cb3b403 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -1307,6 +1307,16 @@
         self.card.set_apdu_parameter(
             cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl)
 
+        for addon_cls in self.profile.addons:
+            addon = addon_cls()
+            if addon.probe(self.card):
+                print("Detected %s Add-on \"%s\"" % (self.profile, addon))
+                for f in addon.files_in_mf:
+                    self.mf.add_file(f)
+
+        # go back to MF before the next steps (addon probing might have changed DF)
+        self.card._scc.select_file('3F00')
+
         # add application ADFs + MF-files from profile
         apps = self._match_applications()
         for a in apps:
diff --git a/pySim/gsm_r.py b/pySim/gsm_r.py
index 389a8cb..cd111d6 100644
--- a/pySim/gsm_r.py
+++ b/pySim/gsm_r.py
@@ -35,8 +35,8 @@
 from pySim.construct import *
 import enum
 
+from pySim.profile import CardProfileAddon
 from pySim.filesystem import *
-import pySim.ts_102_221
 import pySim.ts_51_011
 
 ######################################################################
@@ -362,3 +362,15 @@
                            desc='Free Number Call Type 0 and 8'),
         ]
         self.add_files(files)
+
+
+class AddonGSMR(CardProfileAddon):
+    """An Addon that can be found on either classic GSM SIM or on UICC to support GSM-R."""
+    def __init__(self):
+        files = [
+            DF_EIRENE()
+        ]
+        super().__init__('GSM-R', desc='Railway GSM', files_in_mf=files)
+
+    def probe(self, card: 'CardBase') -> bool:
+        return card.file_exists(self.files_in_mf[0].fid)
diff --git a/pySim/profile.py b/pySim/profile.py
index e464e1f..0d09e81 100644
--- a/pySim/profile.py
+++ b/pySim/profile.py
@@ -88,6 +88,7 @@
                 shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
                 cla : class byte that should be used with cards of this profile
                 sel_ctrl : selection control bytes class byte that should be used with cards of this profile
+                addons: List of optional CardAddons that a card of this profile might have
         """
         self.name = name
         self.desc = kw.get("desc", None)
@@ -97,6 +98,8 @@
         self.shell_cmdsets = kw.get("shell_cmdsets", [])
         self.cla = kw.get("cla", "00")
         self.sel_ctrl = kw.get("sel_ctrl", "0004")
+        # list of optional addons that a card of this profile might have
+        self.addons = kw.get("addons", [])
 
     def __str__(self):
         return self.name
@@ -161,3 +164,34 @@
                 return p()
 
         return None
+
+    def add_addon(self, addon: 'CardProfileAddon'):
+        assert(addon not in self.addons)
+        # we don't install any additional files, as that is happening in the RuntimeState.
+        self.addons.append(addon)
+
+class CardProfileAddon(abc.ABC):
+    """A Card Profile Add-on is something that is not a card application or a full stand-alone
+    card profile, but an add-on to an existing profile.  Think of GSM-R specific files existing
+    on what is otherwise a SIM or USIM+SIM card."""
+
+    def __init__(self, name: str, **kw):
+        """
+        Args:
+                desc (str) : Description
+                files_in_mf : List of CardEF instances present in MF
+                shell_cmdsets : List of cmd2 shell command sets of profile-specific commands
+        """
+        self.name = name
+        self.desc = kw.get("desc", None)
+        self.files_in_mf = kw.get("files_in_mf", [])
+        self.shell_cmdsets = kw.get("shell_cmdsets", [])
+        pass
+
+    def __str__(self):
+        return self.name
+
+    @abc.abstractmethod
+    def probe(self, card: 'CardBase') -> bool:
+        """Probe a given card to determine whether or not this add-on is present/supported."""
+        pass
diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py
index b6c003b..df8b842 100644
--- a/pySim/ts_102_221.py
+++ b/pySim/ts_102_221.py
@@ -31,7 +31,9 @@
 
 # A UICC will usually also support 2G functionality. If this is the case, we
 # need to add DF_GSM and DF_TELECOM along with the UICC related files
-from pySim.ts_51_011 import DF_GSM, DF_TELECOM
+from pySim.ts_51_011 import DF_GSM, DF_TELECOM, AddonSIM
+from pySim.gsm_r import AddonGSMR
+from pySim.cdma_ruim import AddonRUIM
 
 ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
     # TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
@@ -768,6 +770,11 @@
             # FIXME: DF.CD
             EF_UMPC(),
         ]
+        addons = [
+            AddonSIM,
+            AddonGSMR,
+            AddonRUIM,
+        ]
         sw = {
             'Normal': {
                 '9000': 'Normal ending of the command',
@@ -839,7 +846,7 @@
 
         super().__init__(name, desc='ETSI TS 102 221', cla="00",
                          sel_ctrl="0004", files_in_mf=files, sw=sw,
-                         shell_cmdsets = [self.AddlShellCommands()])
+                         shell_cmdsets = [self.AddlShellCommands()], addons = addons)
 
     @staticmethod
     def decode_select_response(resp_hex: str) -> object:
@@ -895,4 +902,5 @@
 
     @staticmethod
     def match_with_card(scc: SimCardCommands) -> bool:
-        return match_uicc(scc) and match_sim(scc)
+        # don't ever select this profile, we only use this from pySim-trace
+        return False
diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py
index 1f98e72..c81bfdf 100644
--- a/pySim/ts_51_011.py
+++ b/pySim/ts_51_011.py
@@ -30,9 +30,10 @@
 #
 
 from pySim.profile import match_sim
-from pySim.profile import CardProfile
+from pySim.profile import CardProfile, CardProfileAddon
 from pySim.filesystem import *
 from pySim.ts_31_102_telecom import DF_PHONEBOOK, DF_MULTIMEDIA, DF_MCS, DF_V2X
+from pySim.gsm_r import AddonGSMR
 import enum
 from pySim.construct import *
 from construct import Optional as COptional
@@ -1047,8 +1048,12 @@
             },
         }
 
+        addons = [
+            AddonGSMR,
+        ]
+
         super().__init__('SIM', desc='GSM SIM Card', cla="a0",
-                         sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw)
+                         sel_ctrl="0000", files_in_mf=[DF_TELECOM(), DF_GSM()], sw=sw, addons = addons)
 
     @staticmethod
     def decode_select_response(resp_hex: str) -> object:
@@ -1104,3 +1109,17 @@
     @staticmethod
     def match_with_card(scc: SimCardCommands) -> bool:
         return match_sim(scc)
+
+
+class AddonSIM(CardProfileAddon):
+    """An add-on that can be found on a UICC in order to support classic GSM SIM."""
+    def __init__(self):
+        files = [
+            DF_GSM(),
+            DF_TELECOM(),
+        ]
+        super().__init__('SIM', desc='GSM SIM', files_in_mf=files)
+
+    def probe(self, card:'CardBase') -> bool:
+        # we assume the add-on to be present in case DF.GSM is found on the card
+        return card.file_exists(self.files_in_mf[0].fid)