laforge has uploaded this change for review. (
https://gerrit.osmocom.org/c/pysim/+/28628
)
Change subject: filesystem: Introduce the basic notion of 'logical channels'
......................................................................
filesystem: Introduce the basic notion of 'logical channels'
cards can have multiple logical channels; each logical channel
has its own state of what is the current selected file + application.
Let's split the RuntimeState class into the global RuntimeState and the
per-lchan-specific RuntimeLchan class.
This code doesn't actually introduce any code that uses lchans other
than the basic logical channel (0), but just modifies the data model
to accomodate those in the future.
Change-Id: I7aa994b625467d4e46a2edd8123240b930305360
---
M pySim-shell.py
M pySim/filesystem.py
M pySim/ts_102_221.py
M pySim/ts_102_222.py
M pySim/ts_31_102.py
M pySim/ts_31_103.py
M pySim/ts_51_011.py
7 files changed, 179 insertions(+), 118 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/28/28628/1
diff --git a/pySim-shell.py b/pySim-shell.py
index dd8b8d0..c05fd7a 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -136,7 +136,8 @@
self.default_category = 'pySim-shell built-in commands'
self.card = None
self.rs = None
- self.py_locals = {'card': self.card, 'rs': self.rs}
+ self.lchan = None
+ self.py_locals = {'card': self.card, 'rs': self.rs,
'lchan': self.lchan}
self.sl = sl
self.ch = ch
@@ -165,7 +166,8 @@
# Unequip everything from pySim-shell that would not work in unequipped state
if self.rs:
- self.rs.unregister_cmds(self)
+ lchan = self.rs.lchan[0]
+ lchan.unregister_cmds(self)
for cmds in [Iso7816Commands, PySimCommands]:
cmd_set = self.find_commandsets(cmds)
if cmd_set:
@@ -177,6 +179,7 @@
# When a card object and a runtime state is present, (re)equip pySim-shell with
everything that is
# needed to operate on cards.
if self.card and self.rs:
+ self.lchan = self.rs.lchan[0]
self._onchange_conserve_write(
'conserve_write', False, self.conserve_write)
self._onchange_apdu_trace('apdu_trace', False, self.apdu_trace)
@@ -184,7 +187,7 @@
self.register_command_set(Ts102222Commands())
self.register_command_set(PySimCommands())
self.iccid, sw = self.card.read_iccid()
- rs.select('MF', self)
+ self.lchan.select('MF', self)
rc = True
else:
self.poutput("pySim-shell not equipped!")
@@ -223,8 +226,8 @@
self.cmd2.poutput("<- %s: %s" % (sw, resp))
def update_prompt(self):
- if self.rs:
- path_list = self.rs.selected_file.fully_qualified_path(
+ if self.lchan:
+ path_list = self.lchan.selected_file.fully_qualified_path(
not self.numeric_path)
self.prompt = 'pySIM-shell (%s)> ' %
('/'.join(path_list))
else:
@@ -477,12 +480,12 @@
else:
flags = ['PARENT', 'SELF', 'FNAMES',
'ANAMES']
selectables = list(
- self._cmd.rs.selected_file.get_selectable_names(flags=flags))
+ self._cmd.lchan.selected_file.get_selectable_names(flags=flags))
directory_str = tabulate_str_list(
selectables, width=79, hspace=2, lspace=1, align_left=True)
- path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
+ path_list = self._cmd.lchan.selected_file.fully_qualified_path(True)
self._cmd.poutput('/'.join(path_list))
- path_list = self._cmd.rs.selected_file.fully_qualified_path(False)
+ path_list = self._cmd.lchan.selected_file.fully_qualified_path(False)
self._cmd.poutput('/'.join(path_list))
self._cmd.poutput(directory_str)
self._cmd.poutput("%d files" % len(selectables))
@@ -490,11 +493,11 @@
def walk(self, indent=0, action_ef=None, action_df=None, context=None, **kwargs):
"""Recursively walk through the file system, starting at the
currently selected DF"""
- if isinstance(self._cmd.rs.selected_file, CardDF):
+ if isinstance(self._cmd.lchan.selected_file, CardDF):
if action_df:
action_df(context, opts)
- files = self._cmd.rs.selected_file.get_selectables(
+ files = self._cmd.lchan.selected_file.get_selectables(
flags=['FNAMES', 'ANAMES'])
for f in files:
# special case: When no action is performed, just output a directory
@@ -511,10 +514,10 @@
if isinstance(files[f], CardDF):
skip_df = False
try:
- fcp_dec = self._cmd.rs.select(f, self._cmd)
+ fcp_dec = self._cmd.lchan.select(f, self._cmd)
except Exception as e:
skip_df = True
- df = self._cmd.rs.selected_file
+ df = self._cmd.lchan.selected_file
df_path_list = df.fully_qualified_path(True)
df_skip_reason_str = '/'.join(df_path_list) + \
"/" + str(f) + ", " + str(e)
@@ -526,17 +529,17 @@
# below, so we must not move up.
if skip_df == False:
self.walk(indent + 1, action_ef, action_df, context, **kwargs)
- fcp_dec = self._cmd.rs.select("..", self._cmd)
+ fcp_dec = self._cmd.lchan.select("..", self._cmd)
elif action_ef:
- df_before_action = self._cmd.rs.selected_file
+ df_before_action = self._cmd.lchan.selected_file
action_ef(f, context, **kwargs)
# When walking through the file system tree the action must not
# always restore the currently selected file to the file that
# was selected before executing the action() callback.
- if df_before_action != self._cmd.rs.selected_file:
+ if df_before_action != self._cmd.lchan.selected_file:
raise RuntimeError("inconsistent walk, %s is currently selected
but expecting %s to be selected"
- % (str(self._cmd.rs.selected_file),
str(df_before_action)))
+ % (str(self._cmd.lchan.selected_file),
str(df_before_action)))
def do_tree(self, opts):
"""Display a filesystem-tree with all selectable
files"""
@@ -545,7 +548,7 @@
def export_ef(self, filename, context, as_json):
""" Select and export a single elementary file (EF)
"""
context['COUNT'] += 1
- df = self._cmd.rs.selected_file
+ df = self._cmd.lchan.selected_file
# The currently selected file (not the file we are going to export)
# must always be an ADF or DF. From this starting point we select
@@ -564,36 +567,36 @@
self._cmd.poutput("# directory: %s (%s)" %
('/'.join(df_path_list),
'/'.join(df_path_list_fid)))
try:
- fcp_dec = self._cmd.rs.select(filename, self._cmd)
+ fcp_dec = self._cmd.lchan.select(filename, self._cmd)
self._cmd.poutput("# file: %s (%s)" % (
- self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
+ self._cmd.lchan.selected_file.name, self._cmd.lchan.selected_file.fid))
- structure = self._cmd.rs.selected_file_structure()
+ structure = self._cmd.lchan.selected_file_structure()
self._cmd.poutput("# structure: %s" % str(structure))
- self._cmd.poutput("# RAW FCP Template: %s" %
str(self._cmd.rs.selected_file_fcp_hex))
- self._cmd.poutput("# Decoded FCP Template: %s" %
str(self._cmd.rs.selected_file_fcp))
+ self._cmd.poutput("# RAW FCP Template: %s" %
str(self._cmd.lchan.selected_file_fcp_hex))
+ self._cmd.poutput("# Decoded FCP Template: %s" %
str(self._cmd.lchan.selected_file_fcp))
for f in df_path_list:
self._cmd.poutput("select " + str(f))
- self._cmd.poutput("select " + self._cmd.rs.selected_file.name)
+ self._cmd.poutput("select " + self._cmd.lchan.selected_file.name)
if structure == 'transparent':
if as_json:
- result = self._cmd.rs.read_binary_dec()
+ result = self._cmd.lchan.read_binary_dec()
self._cmd.poutput("update_binary_decoded '%s'" %
json.dumps(result[0], cls=JsonEncoder))
else:
- result = self._cmd.rs.read_binary()
+ result = self._cmd.lchan.read_binary()
self._cmd.poutput("update_binary " + str(result[0]))
elif structure == 'cyclic' or structure == 'linear_fixed':
# Use number of records specified in select response
- num_of_rec = self._cmd.rs.selected_file_num_of_rec()
+ num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
if num_of_rec:
for r in range(1, num_of_rec + 1):
if as_json:
- result = self._cmd.rs.read_record_dec(r)
+ result = self._cmd.lchan.read_record_dec(r)
self._cmd.poutput("update_record_decoded %d
'%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
- result = self._cmd.rs.read_record(r)
+ result = self._cmd.lchan.read_record(r)
self._cmd.poutput("update_record %d %s" % (r,
str(result[0])))
# When the select response does not return the number of records, read
until we hit the
@@ -603,10 +606,10 @@
while True:
try:
if as_json:
- result = self._cmd.rs.read_record_dec(r)
+ result = self._cmd.lchan.read_record_dec(r)
self._cmd.poutput("update_record_decoded %d
'%s'" % (r, json.dumps(result[0], cls=JsonEncoder)))
else:
- result = self._cmd.rs.read_record(r)
+ result = self._cmd.lchan.read_record(r)
self._cmd.poutput("update_record %d %s" % (r,
str(result[0])))
except SwMatchError as e:
# We are past the last valid record - stop
@@ -617,9 +620,9 @@
raise e
r = r + 1
elif structure == 'ber_tlv':
- tags = self._cmd.rs.retrieve_tags()
+ tags = self._cmd.lchan.retrieve_tags()
for t in tags:
- result = self._cmd.rs.retrieve_data(t)
+ result = self._cmd.lchan.retrieve_data(t)
(tag, l, val, remainer) = bertlv_parse_one(h2b(result[0]))
self._cmd.poutput("set_data 0x%02x %s" % (t, b2h(val)))
else:
@@ -635,8 +638,8 @@
# When reading the file is done, make sure the parent file is
# selected again. This will be the usual case, however we need
# to check before since we must not select the same DF twice
- if df != self._cmd.rs.selected_file:
- self._cmd.rs.select(df.fid or df.aid, self._cmd)
+ if df != self._cmd.lchan.selected_file:
+ self._cmd.lchan.select(df.fid or df.aid, self._cmd)
self._cmd.poutput("#")
@@ -688,13 +691,13 @@
def do_reset(self, opts):
"""Reset the Card."""
- atr = self._cmd.rs.reset(self._cmd)
+ atr = self._cmd.lchan.reset(self._cmd)
self._cmd.poutput('Card ATR: %s' % atr)
self._cmd.update_prompt()
def do_desc(self, opts):
"""Display human readable file description for the currently
selected file"""
- desc = self._cmd.rs.selected_file.desc
+ desc = self._cmd.lchan.selected_file.desc
if desc:
self._cmd.poutput(desc)
else:
@@ -731,21 +734,21 @@
def do_select(self, opts):
"""SELECT a File (ADF/DF/EF)"""
if len(opts.arg_list) == 0:
- path_list = self._cmd.rs.selected_file.fully_qualified_path(True)
- path_list_fid = self._cmd.rs.selected_file.fully_qualified_path(
+ path_list = self._cmd.lchan.selected_file.fully_qualified_path(True)
+ path_list_fid = self._cmd.lchan.selected_file.fully_qualified_path(
False)
self._cmd.poutput("currently selected file: " +
'/'.join(path_list) + " (" +
'/'.join(path_list_fid) + ")")
return
path = opts.arg_list[0]
- fcp_dec = self._cmd.rs.select(path, self._cmd)
+ fcp_dec = self._cmd.lchan.select(path, self._cmd)
self._cmd.update_prompt()
self._cmd.poutput_json(fcp_dec)
def complete_select(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for SELECT"""
- index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+ index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx,
index_dict=index_dict)
def get_code(self, code):
@@ -851,11 +854,11 @@
def do_activate_file(self, opts):
"""Activate the specified EF. This used to be called REHABILITATE
in TS 11.11 for classic
SIM. You need to specify the name or FID of the file to
activate."""
- (data, sw) = self._cmd.rs.activate_file(opts.NAME)
+ (data, sw) = self._cmd.lchan.activate_file(opts.NAME)
def complete_activate_file(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for ACTIVATE
FILE"""
- index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+ index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx,
index_dict=index_dict)
open_chan_parser = argparse.ArgumentParser()
@@ -880,7 +883,7 @@
def do_status(self, opts):
"""Perform the STATUS command."""
- fcp_dec = self._cmd.rs.status()
+ fcp_dec = self._cmd.lchan.status()
self._cmd.poutput_json(fcp_dec)
suspend_uicc_parser = argparse.ArgumentParser()
diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index a3e83d6..bb9f16c 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -49,6 +49,16 @@
# tuple: logical-and of the listed services requires this file
CardFileService = Union[int, List[int], Tuple[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)
+
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.
@@ -545,7 +555,7 @@
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
"""Decode command-line provided hex-string as if it was read
from the file."""
- data = self._cmd.rs.selected_file.decode_hex(opts.HEXSTR)
+ data = self._cmd.lchan.selected_file.decode_hex(opts.HEXSTR)
self._cmd.poutput_json(data, opts.oneline)
read_bin_parser = argparse.ArgumentParser()
@@ -557,7 +567,7 @@
@cmd2.with_argparser(read_bin_parser)
def do_read_binary(self, opts):
"""Read binary data from a transparent EF"""
- (data, sw) = self._cmd.rs.read_binary(opts.length, opts.offset)
+ (data, sw) = self._cmd.lchan.read_binary(opts.length, opts.offset)
self._cmd.poutput(data)
read_bin_dec_parser = argparse.ArgumentParser()
@@ -567,7 +577,7 @@
@cmd2.with_argparser(read_bin_dec_parser)
def do_read_binary_decoded(self, opts):
"""Read + decode data from a transparent EF"""
- (data, sw) = self._cmd.rs.read_binary_dec()
+ (data, sw) = self._cmd.lchan.read_binary_dec()
self._cmd.poutput_json(data, opts.oneline)
upd_bin_parser = argparse.ArgumentParser()
@@ -579,7 +589,7 @@
@cmd2.with_argparser(upd_bin_parser)
def do_update_binary(self, opts):
"""Update (Write) data of a transparent EF"""
- (data, sw) = self._cmd.rs.update_binary(opts.data, opts.offset)
+ (data, sw) = self._cmd.lchan.update_binary(opts.data, opts.offset)
if data:
self._cmd.poutput(data)
@@ -593,18 +603,18 @@
def do_update_binary_decoded(self, opts):
"""Encode + Update (Write) data of a transparent
EF"""
if opts.json_path:
- (data_json, sw) = self._cmd.rs.read_binary_dec()
+ (data_json, sw) = self._cmd.lchan.read_binary_dec()
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
- (data, sw) = self._cmd.rs.update_binary_dec(data_json)
+ (data, sw) = self._cmd.lchan.update_binary_dec(data_json)
if data:
self._cmd.poutput_json(data)
def do_edit_binary_decoded(self, opts):
"""Edit the JSON representation of the EF contents in an
editor."""
- (orig_json, sw) = self._cmd.rs.read_binary_dec()
+ (orig_json, sw) = self._cmd.lchan.read_binary_dec()
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -617,7 +627,7 @@
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
- (data, sw) = self._cmd.rs.update_binary_dec(edited_json)
+ (data, sw) = self._cmd.lchan.update_binary_dec(edited_json)
if data:
self._cmd.poutput_json(data)
@@ -768,7 +778,7 @@
@cmd2.with_argparser(dec_hex_parser)
def do_decode_hex(self, opts):
"""Decode command-line provided hex-string as if it was read
from the file."""
- data = self._cmd.rs.selected_file.decode_record_hex(opts.HEXSTR)
+ data = self._cmd.lchan.selected_file.decode_record_hex(opts.HEXSTR)
self._cmd.poutput_json(data, opts.oneline)
read_rec_parser = argparse.ArgumentParser()
@@ -782,7 +792,7 @@
"""Read one or multiple records from a record-oriented
EF"""
for r in range(opts.count):
recnr = opts.record_nr + r
- (data, sw) = self._cmd.rs.read_record(recnr)
+ (data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
@@ -798,7 +808,7 @@
@cmd2.with_argparser(read_rec_dec_parser)
def do_read_record_decoded(self, opts):
"""Read + decode a record from a record-oriented
EF"""
- (data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
+ (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
self._cmd.poutput_json(data, opts.oneline)
read_recs_parser = argparse.ArgumentParser()
@@ -806,9 +816,9 @@
@cmd2.with_argparser(read_recs_parser)
def do_read_records(self, opts):
"""Read all records from a record-oriented
EF"""
- num_of_rec = self._cmd.rs.selected_file_num_of_rec()
+ num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.rs.read_record(recnr)
+ (data, sw) = self._cmd.lchan.read_record(recnr)
if (len(data) > 0):
recstr = str(data)
else:
@@ -822,11 +832,11 @@
@cmd2.with_argparser(read_recs_dec_parser)
def do_read_records_decoded(self, opts):
"""Read + decode all records from a record-oriented
EF"""
- num_of_rec = self._cmd.rs.selected_file_num_of_rec()
+ num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# collect all results in list so they are rendered as JSON list when
printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.rs.read_record_dec(recnr)
+ (data, sw) = self._cmd.lchan.read_record_dec(recnr)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
@@ -839,7 +849,7 @@
@cmd2.with_argparser(upd_rec_parser)
def do_update_record(self, opts):
"""Update (write) data to a record-oriented
EF"""
- (data, sw) = self._cmd.rs.update_record(opts.record_nr, opts.data)
+ (data, sw) = self._cmd.lchan.update_record(opts.record_nr, opts.data)
if data:
self._cmd.poutput(data)
@@ -855,12 +865,12 @@
def do_update_record_decoded(self, opts):
"""Encode + Update (write) data to a record-oriented
EF"""
if opts.json_path:
- (data_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
+ (data_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
js_path_modify(data_json, opts.json_path,
json.loads(opts.data))
else:
data_json = json.loads(opts.data)
- (data, sw) = self._cmd.rs.update_record_dec(
+ (data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, data_json)
if data:
self._cmd.poutput(data)
@@ -872,7 +882,7 @@
@cmd2.with_argparser(edit_rec_dec_parser)
def do_edit_record_decoded(self, opts):
"""Edit the JSON representation of one record in an
editor."""
- (orig_json, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
+ (orig_json, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname:
filename = '%s/file' % dirname
# write existing data as JSON to file
@@ -885,7 +895,7 @@
if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write")
else:
- (data, sw) = self._cmd.rs.update_record_dec(
+ (data, sw) = self._cmd.lchan.update_record_dec(
opts.record_nr, edited_json)
if data:
self._cmd.poutput_json(data)
@@ -1192,12 +1202,12 @@
@cmd2.with_argparser(retrieve_data_parser)
def do_retrieve_data(self, opts):
"""Retrieve (Read) data from a BER-TLV EF"""
- (data, sw) = self._cmd.rs.retrieve_data(opts.tag)
+ (data, sw) = self._cmd.lchan.retrieve_data(opts.tag)
self._cmd.poutput(data)
def do_retrieve_tags(self, opts):
"""List tags available in a given BER-TLV
EF"""
- tags = self._cmd.rs.retrieve_tags()
+ tags = self._cmd.lchan.retrieve_tags()
self._cmd.poutput(tags)
set_data_parser = argparse.ArgumentParser()
@@ -1209,7 +1219,7 @@
@cmd2.with_argparser(set_data_parser)
def do_set_data(self, opts):
"""Set (Write) data for a given tag in a BER-TLV
EF"""
- (data, sw) = self._cmd.rs.set_data(opts.tag, opts.data)
+ (data, sw) = self._cmd.lchan.set_data(opts.tag, opts.data)
if data:
self._cmd.poutput(data)
@@ -1220,7 +1230,7 @@
@cmd2.with_argparser(del_data_parser)
def do_delete_data(self, opts):
"""Delete data for a given tag in a BER-TLV
EF"""
- (data, sw) = self._cmd.rs.set_data(opts.tag, None)
+ (data, sw) = self._cmd.lchan.set_data(opts.tag, None)
if data:
self._cmd.poutput(data)
@@ -1252,11 +1262,10 @@
"""
self.mf = CardMF(profile=profile)
self.card = card
- self.selected_file = self.mf # type: CardDF
- self.selected_adf = None
self.profile = profile
- self.selected_file_fcp = None
- self.selected_file_fcp_hex = None
+ 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
@@ -1315,6 +1324,66 @@
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:
+ raise ValueError('Cannot create already-existing lchan %d' %
lchan_nr)
+ self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, rs)
+ return self.lchan[lchan_nr]
+
+ def del_lchan(self, lchan_nr: int):
+ if lchan_nr in self.lchan:
+ del 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:
+ 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']
@@ -1330,17 +1399,6 @@
def selected_file_num_of_rec(self) -> Optional[int]:
return
self.selected_file_fcp['file_descriptor'].get('num_of_rec')
- 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)
- """
- atr = i2h(self.card.reset())
- # select MF to reset internal state and to verify card really works
- self.select('MF', cmd_app)
- self.selected_adf = None
- return atr
-
def get_cwd(self) -> CardDF:
"""Obtain the current working directory.
@@ -1384,7 +1442,7 @@
# card profile.
if app and hasattr(app, "interpret_sw"):
res = app.interpret_sw(sw)
- return res or self.profile.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
@@ -1394,7 +1452,7 @@
"Cannot select unknown file by name %s, only hexadecimal 4 digit FID
is allowed" % fid)
try:
- (data, sw) = self.card._scc.select_file(fid)
+ (data, sw) = self.rs.card._scc.select_file(fid)
except SwMatchError as swm:
k = self.interpret_sw(swm.sw_actual)
if not k:
@@ -1446,10 +1504,10 @@
for p in inter_path:
try:
if isinstance(p, CardADF):
- (data, sw) = self.card.select_adf_by_aid(p.aid)
+ (data, sw) = self.rs.card.select_adf_by_aid(p.aid)
self.selected_adf = p
else:
- (data, sw) = self.card._scc.select_file(p.fid)
+ (data, sw) = self.rs.card._scc.select_file(p.fid)
self.selected_file = p
except SwMatchError as swm:
self._select_post(cmd_app)
@@ -1490,9 +1548,9 @@
f = sels[name]
try:
if isinstance(f, CardADF):
- (data, sw) = self.card.select_adf_by_aid(f.aid)
+ (data, sw) = self.rs.card.select_adf_by_aid(f.aid)
else:
- (data, sw) = self.card._scc.select_file(f.fid)
+ (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)
@@ -1512,7 +1570,7 @@
def status(self):
"""Request STATUS (current selected file FCP) from
card."""
- (data, sw) = self.card._scc.status()
+ (data, sw) = self.rs.card._scc.status()
return self.selected_file.decode_select_response(data)
def get_file_for_selectable(self, name: str):
@@ -1523,7 +1581,7 @@
"""Request ACTIVATE FILE of specified file."""
sels = self.selected_file.get_selectables()
f = sels[name]
- data, sw = self.card._scc.activate_file(f.fid)
+ data, sw = self.rs.card._scc.activate_file(f.fid)
return data, sw
def read_binary(self, length: int = None, offset: int = 0):
@@ -1537,7 +1595,7 @@
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
- return self.card._scc.read_binary(self.selected_file.fid, length, offset)
+ 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.
@@ -1561,7 +1619,7 @@
"""
if not isinstance(self.selected_file, TransparentEF):
raise TypeError("Only works with TransparentEF")
- return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset,
conserve=self.conserve_write)
+ return self.rs.card._scc.update_binary(self.selected_file.fid, data_hex, offset,
conserve=self.conserve_write)
def update_binary_dec(self, data: dict):
"""Update transparent EF from abstract data. Encodes the data to
binary and
@@ -1584,7 +1642,7 @@
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
# returns a string of hex nibbles
- return self.card._scc.read_record(self.selected_file.fid, rec_nr)
+ 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.
@@ -1606,7 +1664,7 @@
"""
if not isinstance(self.selected_file, LinFixedEF):
raise TypeError("Only works with Linear Fixed EF")
- return self.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex,
conserve=self.conserve_write)
+ return self.rs.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex,
conserve=self.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
@@ -1630,7 +1688,7 @@
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
# returns a string of hex nibbles
- return self.card._scc.retrieve_data(self.selected_file.fid, tag)
+ return self.rs.card._scc.retrieve_data(self.selected_file.fid, tag)
def retrieve_tags(self):
"""Retrieve tags available on BER-TLV EF.
@@ -1640,7 +1698,7 @@
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
- data, sw = self.card._scc.retrieve_data(self.selected_file.fid, 0x5c)
+ 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)
@@ -1653,7 +1711,7 @@
"""
if not isinstance(self.selected_file, BerTlvEF):
raise TypeError("Only works with BER-TLV EF")
- return self.card._scc.set_data(self.selected_file.fid, tag, data_hex,
conserve=self.conserve_write)
+ return self.rs.card._scc.set_data(self.selected_file.fid, tag, data_hex,
conserve=self.conserve_write)
def unregister_cmds(self, cmd_app=None):
"""Unregister all file specific commands."""
diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py
index 08e836c..8578f68 100644
--- a/pySim/ts_102_221.py
+++ b/pySim/ts_102_221.py
@@ -686,19 +686,19 @@
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
def do_read_arr_record(self, opts):
"""Read one EF.ARR record in flattened, human-friendly
form."""
- (data, sw) = self._cmd.rs.read_record_dec(opts.record_nr)
- data = self._cmd.rs.selected_file.flatten(data)
+ (data, sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
+ data = self._cmd.lchan.selected_file.flatten(data)
self._cmd.poutput_json(data, opts.oneline)
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
def do_read_arr_records(self, opts):
"""Read + decode all EF.ARR records in flattened,
human-friendly form."""
- num_of_rec = self._cmd.rs.selected_file_num_of_rec()
+ num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
# collect all results in list so they are rendered as JSON list when
printing
data_list = []
for recnr in range(1, 1 + num_of_rec):
- (data, sw) = self._cmd.rs.read_record_dec(recnr)
- data = self._cmd.rs.selected_file.flatten(data)
+ (data, sw) = self._cmd.lchan.read_record_dec(recnr)
+ data = self._cmd.lchan.selected_file.flatten(data)
data_list.append(data)
self._cmd.poutput_json(data_list, opts.oneline)
diff --git a/pySim/ts_102_222.py b/pySim/ts_102_222.py
index 3c57e5b..4843c77 100644
--- a/pySim/ts_102_222.py
+++ b/pySim/ts_102_222.py
@@ -51,12 +51,12 @@
if not opts.force_delete:
self._cmd.perror("Refusing to permanently delete the file, please read
the help text.")
return
- f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+ f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.delete_file(f.fid)
def complete_delete_file(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for DELETE FILE"""
- index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+ index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx,
index_dict=index_dict)
termdf_parser = argparse.ArgumentParser()
@@ -72,12 +72,12 @@
if not opts.force:
self._cmd.perror("Refusing to terminate the file, please read the help
text.")
return
- f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+ f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.terminate_df(f.fid)
def complete_terminate_df(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE DF"""
- index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+ index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx,
index_dict=index_dict)
@cmd2.with_argparser(termdf_parser)
@@ -88,12 +88,12 @@
if not opts.force:
self._cmd.perror("Refusing to terminate the file, please read the help
text.")
return
- f = self._cmd.rs.get_file_for_selectable(opts.NAME)
+ f = self._cmd.lchan.get_file_for_selectable(opts.NAME)
(data, sw) = self._cmd.card._scc.terminate_ef(f.fid)
def complete_terminate_ef(self, text, line, begidx, endidx) -> List[str]:
"""Command Line tab completion for TERMINATE EF"""
- index_dict = {1: self._cmd.rs.selected_file.get_selectable_names()}
+ index_dict = {1: self._cmd.lchan.selected_file.get_selectable_names()}
return self._cmd.index_based_complete(text, line, begidx, endidx,
index_dict=index_dict)
tcard_parser = argparse.ArgumentParser()
@@ -154,7 +154,7 @@
fcp = FcpTemplate(children=ies)
(data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows
nothing of it
- self._cmd.rs.select_file(self._cmd.rs.selected_file)
+ self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
createdf_parser = argparse.ArgumentParser()
createdf_parser.add_argument('FILE_ID', type=str, help='File Identifier
as 4-character hex string')
@@ -205,4 +205,4 @@
fcp = FcpTemplate(children=ies)
(data, sw) = self._cmd.card._scc.create_file(b2h(fcp.to_tlv()))
# the newly-created file is automatically selected but our runtime state knows
nothing of it
- self._cmd.rs.select_file(self._cmd.rs.selected_file)
+ self._cmd.lchan.select_file(self._cmd.lchan.selected_file)
diff --git a/pySim/ts_31_102.py b/pySim/ts_31_102.py
index 2147f5e..ea58c20 100644
--- a/pySim/ts_31_102.py
+++ b/pySim/ts_31_102.py
@@ -588,7 +588,7 @@
def get_active_services(self, cmd):
# obtain list of currently active services
- (service_data, sw) = cmd.rs.read_binary_dec()
+ (service_data, sw) = cmd.lchan.read_binary_dec()
active_services = []
for s in service_data.keys():
if service_data[s]['activated']:
@@ -609,7 +609,7 @@
for f in files_by_service[s]:
should_exist = f.should_exist_for_services(active_services)
try:
- cmd.rs.select_file(f)
+ cmd.lchan.select_file(f)
sw = None
exists = True
except SwMatchError as e:
@@ -623,7 +623,7 @@
cmd.perror(" ERROR: File %s is not selectable (%s) but
should!" % (f, sw))
finally:
# re-select the EF.UST
- cmd.rs.select_file(self)
+ cmd.lchan.select_file(self)
return num_problems
class EF_UST(EF_UServiceTable):
@@ -652,7 +652,7 @@
absent/deactivated. This performs a consistency check to ensure that no
services are activated
for files that are not - and vice-versa, no files are activated for services
that are not. Error
messages are printed for every inconsistency found."""
- selected_file = self._cmd.rs.selected_file
+ selected_file = self._cmd.lchan.selected_file
num_problems = selected_file.ust_service_check(self._cmd)
# obtain list of currently active services
active_services = selected_file.get_active_services(self._cmd)
diff --git a/pySim/ts_31_103.py b/pySim/ts_31_103.py
index a4f7983..fa9c851 100644
--- a/pySim/ts_31_103.py
+++ b/pySim/ts_31_103.py
@@ -131,7 +131,7 @@
absent/deactivated. This performs a consistency check to ensure that no
services are activated
for files that are not - and vice-versa, no files are activated for services
that are not. Error
messages are printed for every inconsistency found."""
- selected_file = self._cmd.rs.selected_file
+ selected_file = self._cmd.lchan.selected_file
num_problems = selected_file.ust_service_check(self._cmd)
self._cmd.poutput("===> %u service / file inconsistencies
detected" % num_problems)
diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py
index 854d9ad..585ec11 100644
--- a/pySim/ts_51_011.py
+++ b/pySim/ts_51_011.py
@@ -542,15 +542,15 @@
"""Change the plmn part of the IMSI"""
plmn = arg.strip()
if len(plmn) == 5 or len(plmn) == 6:
- (data, sw) = self._cmd.rs.read_binary_dec()
+ (data, sw) = self._cmd.lchan.read_binary_dec()
if sw == '9000' and len(data['imsi'])-len(plmn) == 10:
imsi = data['imsi']
msin = imsi[len(plmn):]
- (data, sw) = self._cmd.rs.update_binary_dec(
+ (data, sw) = self._cmd.lchan.update_binary_dec(
{'imsi': plmn+msin})
if sw == '9000' and data:
self._cmd.poutput_json(
- self._cmd.rs.selected_file.decode_hex(data))
+ self._cmd.lchan.selected_file.decode_hex(data))
else:
raise ValueError("PLMN length does not match IMSI length")
else:
--
To view, visit
https://gerrit.osmocom.org/c/pysim/+/28628
To unsubscribe, or for help writing mail filters, visit
https://gerrit.osmocom.org/settings
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I7aa994b625467d4e46a2edd8123240b930305360
Gerrit-Change-Number: 28628
Gerrit-PatchSet: 1
Gerrit-Owner: laforge <laforge(a)osmocom.org>
Gerrit-MessageType: newchange