ts_102_221: Implement File Descriptor using construct

This automatically adds encoding support, which is needed for upcoming
CREATE FILE support.

Change-Id: Ia40dba4aab6ceb9d81fd170f7efa8dad1f9b43d0
diff --git a/docs/shell.rst b/docs/shell.rst
index 3ab1113..58588a2 100644
--- a/docs/shell.rst
+++ b/docs/shell.rst
@@ -80,9 +80,11 @@
   pySIM-shell (MF)> select ADF.USIM
   {
       "file_descriptor": {
-          "shareable": true,
-          "file_type": "df",
-          "structure": "no_info_given"
+          "file_descriptor_byte": {
+              "shareable": true,
+              "file_type": "df",
+              "structure": "no_info_given"
+          }
       },
       "df_name": "A0000000871002FFFFFFFF8907090000",
       "proprietary_info": {
diff --git a/pySim-shell.py b/pySim-shell.py
index a82d56f..e58151b 100755
--- a/pySim-shell.py
+++ b/pySim-shell.py
@@ -528,8 +528,7 @@
             self._cmd.poutput("# file: %s (%s)" % (
                 self._cmd.rs.selected_file.name, self._cmd.rs.selected_file.fid))
 
-            fd = fcp_dec['file_descriptor']
-            structure = fd['structure']
+            structure = self._cmd.rs.selected_file_structure()
             self._cmd.poutput("# structure: %s" % str(structure))
 
             for f in df_path_list:
@@ -545,8 +544,8 @@
                     self._cmd.poutput("update_binary " + str(result[0]))
             elif structure == 'cyclic' or structure == 'linear_fixed':
                 # Use number of records specified in select response
-                if 'num_of_rec' in fd:
-                    num_of_rec = fd['num_of_rec']
+                num_of_rec = self._cmd.rs.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)
diff --git a/pySim/filesystem.py b/pySim/filesystem.py
index bef9005..a354dfb 100644
--- a/pySim/filesystem.py
+++ b/pySim/filesystem.py
@@ -773,7 +773,7 @@
         @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_fcp['file_descriptor']['num_of_rec']
+            num_of_rec = self._cmd.rs.selected_file_num_of_rec()
             for recnr in range(1, 1 + num_of_rec):
                 (data, sw) = self._cmd.rs.read_record(recnr)
                 if (len(data) > 0):
@@ -789,7 +789,7 @@
         @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_fcp['file_descriptor']['num_of_rec']
+            num_of_rec = self._cmd.rs.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):
@@ -1279,6 +1279,21 @@
                 pass
         return apps_taken
 
+    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 reset(self, cmd_app=None) -> Hexstr:
         """Perform physical card reset and obtain ATR.
         Args:
@@ -1350,11 +1365,11 @@
             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_type'] == 'df'):
+        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']['structure'] == 'transparent'):
+            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:
diff --git a/pySim/ts_102_221.py b/pySim/ts_102_221.py
index 82776a0..022a4a7 100644
--- a/pySim/ts_102_221.py
+++ b/pySim/ts_102_221.py
@@ -18,6 +18,7 @@
 """
 
 from construct import *
+from construct import Optional as COptional
 from pySim.construct import *
 from pySim.utils import *
 from pySim.filesystem import *
@@ -87,34 +88,22 @@
 
 # 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
+    class BerTlvAdapter(Adapter):
+        def _parse(self, obj, context, path):
+            if obj == 0x39:
+                return 'ber_tlv'
+            raise ValidationError
+        def _build(self, obj, context, path):
+            if obj == 'ber_tlv':
+                return 0x39
+            raise ValidationError
+
+    FDB = Select(BitStruct(Const(0, Bit), 'shareable'/Flag, 'structure'/BerTlvAdapter(Const(0x39, BitsInteger(6)))),
+                 BitStruct(Const(0, Bit), 'shareable'/Flag, 'file_type'/Enum(BitsInteger(3), working_ef=0, internal_ef=1, df=7),
+                           'structure'/Enum(BitsInteger(3), no_info_given=0, transparent=1, linear_fixed=2, cyclic=6))
+                )
+    _construct = Struct('file_descriptor_byte'/FDB, Const(b'\x21'),
+                        'record_len'/COptional(Int16ub), 'num_of_rec'/COptional(Int16ub))
 
 # ETSI TS 102 221 11.1.1.4.4
 class FileIdentifier(BER_TLV_IE, tag=0x83):
@@ -668,7 +657,7 @@
         @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_fcp['file_descriptor']['num_of_rec']
+            num_of_rec = self._cmd.rs.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):
diff --git a/pySim/ts_51_011.py b/pySim/ts_51_011.py
index 5e430ea..ddfed95 100644
--- a/pySim/ts_51_011.py
+++ b/pySim/ts_51_011.py
@@ -1109,7 +1109,9 @@
             4: 'working_ef'
         }
         ret = {
-            'file_descriptor': {},
+            'file_descriptor': {
+                'file_descriptor_byte': {},
+            },
             'proprietary_info': {},
         }
         ret['file_id'] = b2h(resp_bin[4:6])
@@ -1117,7 +1119,7 @@
             resp_bin[2:4], 'big')
         file_type = type_of_file_map[resp_bin[6]
                                      ] if resp_bin[6] in type_of_file_map else resp_bin[6]
-        ret['file_descriptor']['file_type'] = file_type
+        ret['file_descriptor']['file_descriptor_byte']['file_type'] = file_type
         if file_type in ['mf', 'df']:
             ret['file_characteristics'] = b2h(resp_bin[13:14])
             ret['num_direct_child_df'] = resp_bin[14]
@@ -1127,7 +1129,7 @@
         elif file_type in ['working_ef']:
             file_struct = struct_of_file_map[resp_bin[13]
                                              ] if resp_bin[13] in struct_of_file_map else resp_bin[13]
-            ret['file_descriptor']['structure'] = file_struct
+            ret['file_descriptor']['file_descriptor_byte']['structure'] = file_struct
             ret['access_conditions'] = b2h(resp_bin[8:10])
             if resp_bin[11] & 0x01 == 0:
                 ret['life_cycle_status_int'] = 'operational_activated'