move Runtime{State,Lchan} from pySim.filesystem to new pySim.runtime
Those two are really separate concepts, so let's keep them in separate
source code files.
Change-Id: I9ec54304dd8f4a4cba9487054a8eb8d265c2d340
diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index 2bcd363..9f3ee17 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -51,18 +51,6 @@
Size = Tuple[int, Optional[int]]
-def lchan_nr_from_cla(cla: int) -> int:
- """Resolve the logical channel number from the CLA byte."""
- # TS 102 221 10.1.1 Coding of Class Byte
- if cla >> 4 in [0x0, 0xA, 0x8]:
- # Table 10.3
- return cla & 0x03
- elif cla & 0xD0 in [0x40, 0xC0]:
- # Table 10.4a
- return 4 + (cla & 0x0F)
- else:
- raise ValueError('Could not determine logical channel for CLA=%2X' % cla)
-
class CardFile:
"""Base class for all objects in the smart card filesystem.
Serve as a common ancestor to all other file types; rarely used directly.
@@ -1285,486 +1273,6 @@
self.size = size
self.shell_commands = [self.ShellCommands()]
-
-class RuntimeState:
- """Represent the runtime state of a session with a card."""
-
- def __init__(self, card, profile: 'CardProfile'):
- """
- Args:
- card : pysim.cards.Card instance
- profile : CardProfile instance
- """
- self.mf = CardMF(profile=profile)
- self.card = card
- self.profile = profile
- self.lchan = {}
- # the basic logical channel always exists
- self.lchan[0] = RuntimeLchan(0, self)
-
- # make sure the class and selection control bytes, which are specified
- # by the card profile are used
- 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:
- if a.adf:
- self.mf.add_application_df(a.adf)
- for f in self.profile.files_in_mf:
- self.mf.add_file(f)
- self.conserve_write = True
-
- # make sure that when the runtime state is created, the card is also
- # in a defined state.
- self.reset()
-
- def _match_applications(self):
- """match the applications from the profile with applications on the card"""
- apps_profile = self.profile.applications
-
- # When the profile does not feature any applications, then we are done already
- if not apps_profile:
- return []
-
- # Read AIDs from card and match them against the applications defined by the
- # card profile
- aids_card = self.card.read_aids()
- apps_taken = []
- if aids_card:
- aids_taken = []
- print("AIDs on card:")
- for a in aids_card:
- for f in apps_profile:
- if f.aid in a:
- print(" %s: %s (EF.DIR)" % (f.name, a))
- aids_taken.append(a)
- apps_taken.append(f)
- aids_unknown = set(aids_card) - set(aids_taken)
- for a in aids_unknown:
- print(" unknown: %s (EF.DIR)" % a)
- else:
- print("warning: EF.DIR seems to be empty!")
-
- # Some card applications may not be registered in EF.DIR, we will actively
- # probe for those applications
- for f in set(apps_profile) - set(apps_taken):
- try:
- data, sw = self.card.select_adf_by_aid(f.aid)
- if sw == "9000":
- print(" %s: %s" % (f.name, f.aid))
- apps_taken.append(f)
- except (SwMatchError, ProtocolError):
- pass
- return apps_taken
-
- def reset(self, cmd_app=None) -> Hexstr:
- """Perform physical card reset and obtain ATR.
- Args:
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # delete all lchan != 0 (basic lchan)
- for lchan_nr in self.lchan.keys():
- if lchan_nr == 0:
- continue
- del self.lchan[lchan_nr]
- atr = i2h(self.card.reset())
- # select MF to reset internal state and to verify card really works
- self.lchan[0].select('MF', cmd_app)
- self.lchan[0].selected_adf = None
- return atr
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a logical channel to the runtime state. You shouldn't call this
- directly but always go through RuntimeLchan.add_lchan()."""
- if lchan_nr in self.lchan.keys():
- raise ValueError('Cannot create already-existing lchan %d' % lchan_nr)
- self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self)
- return self.lchan[lchan_nr]
-
- def del_lchan(self, lchan_nr: int):
- if lchan_nr in self.lchan.keys():
- del self.lchan[lchan_nr]
- return True
- else:
- return False
-
- def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']:
- lchan_nr = lchan_nr_from_cla(cla)
- if lchan_nr in self.lchan.keys():
- return self.lchan[lchan_nr]
- else:
- return None
-
-
-class RuntimeLchan:
- """Represent the runtime state of a logical channel with a card."""
-
- def __init__(self, lchan_nr: int, rs: RuntimeState):
- self.lchan_nr = lchan_nr
- self.rs = rs
- self.selected_file = self.rs.mf
- self.selected_adf = None
- self.selected_file_fcp = None
- self.selected_file_fcp_hex = None
-
- def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan':
- """Add a new logical channel from the current logical channel. Just affects
- internal state, doesn't actually open a channel with the UICC."""
- new_lchan = self.rs.add_lchan(lchan_nr)
- # See TS 102 221 Table 8.3
- if self.lchan_nr != 0:
- new_lchan.selected_file = self.get_cwd()
- new_lchan.selected_adf = self.selected_adf
- return new_lchan
-
- def selected_file_descriptor_byte(self) -> dict:
- return self.selected_file_fcp['file_descriptor']['file_descriptor_byte']
-
- def selected_file_shareable(self) -> bool:
- return self.selected_file_descriptor_byte()['shareable']
-
- def selected_file_structure(self) -> str:
- return self.selected_file_descriptor_byte()['structure']
-
- def selected_file_type(self) -> str:
- return self.selected_file_descriptor_byte()['file_type']
-
- def selected_file_num_of_rec(self) -> Optional[int]:
- return self.selected_file_fcp['file_descriptor'].get('num_of_rec')
-
- def get_cwd(self) -> CardDF:
- """Obtain the current working directory.
-
- Returns:
- CardDF instance
- """
- if isinstance(self.selected_file, CardDF):
- return self.selected_file
- else:
- return self.selected_file.parent
-
- def get_application_df(self) -> Optional[CardADF]:
- """Obtain the currently selected application DF (if any).
-
- Returns:
- CardADF() instance or None"""
- # iterate upwards from selected file; check if any is an ADF
- node = self.selected_file
- while node.parent != node:
- if isinstance(node, CardADF):
- return node
- node = node.parent
- return None
-
- def interpret_sw(self, sw: str):
- """Interpret a given status word relative to the currently selected application
- or the underlying card profile.
-
- Args:
- sw : Status word as string of 4 hex digits
-
- Returns:
- Tuple of two strings
- """
- res = None
- adf = self.get_application_df()
- if adf:
- app = adf.application
- # The application either comes with its own interpret_sw
- # method or we will use the interpret_sw method from the
- # card profile.
- if app and hasattr(app, "interpret_sw"):
- res = app.interpret_sw(sw)
- return res or self.rs.profile.interpret_sw(sw)
-
- def probe_file(self, fid: str, cmd_app=None):
- """Blindly try to select a file and automatically add a matching file
- object if the file actually exists."""
- if not is_hex(fid, 4, 4):
- raise ValueError(
- "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid)
-
- try:
- (data, sw) = self.rs.card._scc.select_file(fid)
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
-
- select_resp = self.selected_file.decode_select_response(data)
- if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'):
- f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(),
- desc="dedicated file, manually added at runtime")
- else:
- if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'):
- f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
- else:
- f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(),
- desc="elementary file, manually added at runtime")
-
- self.selected_file.add_files([f])
- self.selected_file = f
- return select_resp, data
-
- def _select_pre(self, cmd_app):
- # unregister commands of old file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
- def _select_post(self, cmd_app):
- # register commands of new file
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.register_command_set(c)
-
- def select_file(self, file: CardFile, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- file : CardFile [or derived class] instance
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # we need to find a path from our self.selected_file to the destination
- inter_path = self.selected_file.build_select_path_to(file)
- if not inter_path:
- raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file))
-
- self._select_pre(cmd_app)
-
- for p in inter_path:
- try:
- if isinstance(p, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
- self.selected_adf = p
- else:
- (data, sw) = self.rs.card._scc.select_file(p.fid)
- self.selected_file = p
- except SwMatchError as swm:
- self._select_post(cmd_app)
- raise(swm)
-
- self._select_post(cmd_app)
-
- def select(self, name: str, cmd_app=None):
- """Select a file (EF, DF, ADF, MF, ...).
-
- Args:
- name : Name of file to select
- cmd_app : Command Application State (for unregistering old file commands)
- """
- # handling of entire paths with multiple directories/elements
- if '/' in name:
- prev_sel_file = self.selected_file
- pathlist = name.split('/')
- # treat /DF.GSM/foo like MF/DF.GSM/foo
- if pathlist[0] == '':
- pathlist[0] = 'MF'
- try:
- for p in pathlist:
- self.select(p, cmd_app)
- return
- except Exception as e:
- # if any intermediate step fails, go back to where we were
- self.select_file(prev_sel_file, cmd_app)
- raise e
-
- sels = self.selected_file.get_selectables()
- if is_hex(name):
- name = name.lower()
-
- self._select_pre(cmd_app)
-
- if name in sels:
- f = sels[name]
- try:
- if isinstance(f, CardADF):
- (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
- else:
- (data, sw) = self.rs.card._scc.select_file(f.fid)
- self.selected_file = f
- except SwMatchError as swm:
- k = self.interpret_sw(swm.sw_actual)
- if not k:
- raise(swm)
- raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1]))
- select_resp = f.decode_select_response(data)
- else:
- (select_resp, data) = self.probe_file(name, cmd_app)
-
- # store the raw + decoded FCP for later reference
- self.selected_file_fcp_hex = data
- self.selected_file_fcp = select_resp
-
- self._select_post(cmd_app)
- return select_resp
-
- def status(self):
- """Request STATUS (current selected file FCP) from card."""
- (data, sw) = self.rs.card._scc.status()
- return self.selected_file.decode_select_response(data)
-
- def get_file_for_selectable(self, name: str):
- sels = self.selected_file.get_selectables()
- return sels[name]
-
- def activate_file(self, name: str):
- """Request ACTIVATE FILE of specified file."""
- sels = self.selected_file.get_selectables()
- f = sels[name]
- data, sw = self.rs.card._scc.activate_file(f.fid)
- return data, sw
-
- def read_binary(self, length: int = None, offset: int = 0):
- """Read [part of] a transparent EF binary data.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- binary data read from the file
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.read_binary(self.selected_file.fid, length, offset)
-
- def read_binary_dec(self) -> Tuple[dict, str]:
- """Read [part of] a transparent EF binary data and decode it.
-
- Args:
- length : Amount of data to read (None: as much as possible)
- offset : Offset into the file from which to read 'length' bytes
- Returns:
- abstract decode data read from the file
- """
- (data, sw) = self.read_binary()
- dec_data = self.selected_file.decode_hex(data)
- return (dec_data, sw)
-
- def update_binary(self, data_hex: str, offset: int = 0):
- """Update transparent EF binary data.
-
- Args:
- data_hex : hex string of data to be written
- offset : Offset into the file from which to write 'data_hex'
- """
- if not isinstance(self.selected_file, TransparentEF):
- raise TypeError("Only works with TransparentEF")
- return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write)
-
- def update_binary_dec(self, data: dict):
- """Update transparent EF from abstract data. Encodes the data to binary and
- then updates the EF with it.
-
- Args:
- data : abstract data which is to be encoded and written
- """
- data_hex = self.selected_file.encode_hex(data)
- return self.update_binary(data_hex)
-
- def read_record(self, rec_nr: int = 0):
- """Read a record as binary data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- hex string of binary data contained in record
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.read_record(self.selected_file.fid, rec_nr)
-
- def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]:
- """Read a record and decode it to abstract data.
-
- Args:
- rec_nr : Record number to read
- Returns:
- abstract data contained in record
- """
- (data, sw) = self.read_record(rec_nr)
- return (self.selected_file.decode_record_hex(data, rec_nr), sw)
-
- def update_record(self, rec_nr: int, data_hex: str):
- """Update a record with given binary data
-
- Args:
- rec_nr : Record number to read
- data_hex : Hex string binary data to be written
- """
- if not isinstance(self.selected_file, LinFixedEF):
- raise TypeError("Only works with Linear Fixed EF")
- return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.rs.conserve_write)
-
- def update_record_dec(self, rec_nr: int, data: dict):
- """Update a record with given abstract data. Will encode abstract to binary data
- and then write it to the given record on the card.
-
- Args:
- rec_nr : Record number to read
- data_hex : Abstract data to be written
- """
- data_hex = self.selected_file.encode_record_hex(data, rec_nr)
- return self.update_record(rec_nr, data_hex)
-
- def retrieve_data(self, tag: int = 0):
- """Read a DO/TLV as binary data.
-
- Args:
- tag : Tag of TLV/DO to read
- Returns:
- hex string of full BER-TLV DO including Tag and Length
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- # returns a string of hex nibbles
- return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
-
- def retrieve_tags(self):
- """Retrieve tags available on BER-TLV EF.
-
- Returns:
- list of integer tags contained in EF
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- data, sw = self.rs.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
- tag, length, value, remainder = bertlv_parse_one(h2b(data))
- return list(value)
-
- def set_data(self, tag: int, data_hex: str):
- """Update a TLV/DO with given binary data
-
- Args:
- tag : Tag of TLV/DO to be written
- data_hex : Hex string binary data to be written (value portion)
- """
- if not isinstance(self.selected_file, BerTlvEF):
- raise TypeError("Only works with BER-TLV EF")
- return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write)
-
- def unregister_cmds(self, cmd_app=None):
- """Unregister all file specific commands."""
- if cmd_app and self.selected_file.shell_commands:
- for c in self.selected_file.shell_commands:
- cmd_app.unregister_command_set(c)
-
-
def interpret_sw(sw_data: dict, sw: str):
"""Interpret a given status word.
@@ -1828,7 +1336,7 @@
@classmethod
@abc.abstractmethod
- def add_files(cls, rs: RuntimeState):
+ def add_files(cls, rs: 'RuntimeState'):
"""Add model specific files to given RuntimeState."""
@classmethod
@@ -1843,7 +1351,7 @@
return False
@staticmethod
- def apply_matching_models(scc: SimCardCommands, rs: RuntimeState):
+ def apply_matching_models(scc: SimCardCommands, rs: 'RuntimeState'):
"""Check if any of the CardModel sub-classes 'match' the currently inserted card
(by ATR or overriding the 'match' method). If so, call their 'add_files'
method."""