Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 1 | # coding=utf-8 |
| 2 | """Representation of the runtime state of an application like pySim-shell. |
| 3 | """ |
| 4 | |
| 5 | # (C) 2021 by Harald Welte <laforge@osmocom.org> |
| 6 | # |
| 7 | # This program is free software: you can redistribute it and/or modify |
| 8 | # it under the terms of the GNU General Public License as published by |
| 9 | # the Free Software Foundation, either version 2 of the License, or |
| 10 | # (at your option) any later version. |
| 11 | # |
| 12 | # This program is distributed in the hope that it will be useful, |
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | # GNU General Public License for more details. |
| 16 | # |
| 17 | # You should have received a copy of the GNU General Public License |
| 18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 19 | |
| 20 | from typing import Optional, Tuple |
| 21 | |
| 22 | from pySim.utils import sw_match, h2b, i2h, is_hex, bertlv_parse_one, Hexstr |
| 23 | from pySim.exceptions import * |
| 24 | from pySim.filesystem import * |
| 25 | |
| 26 | def lchan_nr_from_cla(cla: int) -> int: |
| 27 | """Resolve the logical channel number from the CLA byte.""" |
| 28 | # TS 102 221 10.1.1 Coding of Class Byte |
| 29 | if cla >> 4 in [0x0, 0xA, 0x8]: |
| 30 | # Table 10.3 |
| 31 | return cla & 0x03 |
| 32 | elif cla & 0xD0 in [0x40, 0xC0]: |
| 33 | # Table 10.4a |
| 34 | return 4 + (cla & 0x0F) |
| 35 | else: |
| 36 | raise ValueError('Could not determine logical channel for CLA=%2X' % cla) |
| 37 | |
| 38 | class RuntimeState: |
| 39 | """Represent the runtime state of a session with a card.""" |
| 40 | |
Harald Welte | 49acc06 | 2023-10-21 20:21:51 +0200 | [diff] [blame] | 41 | def __init__(self, card: 'CardBase', profile: 'CardProfile'): |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 42 | """ |
| 43 | Args: |
| 44 | card : pysim.cards.Card instance |
| 45 | profile : CardProfile instance |
| 46 | """ |
| 47 | self.mf = CardMF(profile=profile) |
| 48 | self.card = card |
| 49 | self.profile = profile |
| 50 | self.lchan = {} |
| 51 | # the basic logical channel always exists |
| 52 | self.lchan[0] = RuntimeLchan(0, self) |
| 53 | |
| 54 | # make sure the class and selection control bytes, which are specified |
| 55 | # by the card profile are used |
| 56 | self.card.set_apdu_parameter( |
| 57 | cla=self.profile.cla, sel_ctrl=self.profile.sel_ctrl) |
| 58 | |
| 59 | for addon_cls in self.profile.addons: |
| 60 | addon = addon_cls() |
| 61 | if addon.probe(self.card): |
| 62 | print("Detected %s Add-on \"%s\"" % (self.profile, addon)) |
| 63 | for f in addon.files_in_mf: |
| 64 | self.mf.add_file(f) |
| 65 | |
| 66 | # go back to MF before the next steps (addon probing might have changed DF) |
Philipp Maier | b8b61bf | 2023-12-07 10:39:21 +0100 | [diff] [blame] | 67 | self.lchan[0].select('MF') |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 68 | |
| 69 | # add application ADFs + MF-files from profile |
| 70 | apps = self._match_applications() |
| 71 | for a in apps: |
| 72 | if a.adf: |
| 73 | self.mf.add_application_df(a.adf) |
| 74 | for f in self.profile.files_in_mf: |
| 75 | self.mf.add_file(f) |
| 76 | self.conserve_write = True |
| 77 | |
| 78 | # make sure that when the runtime state is created, the card is also |
| 79 | # in a defined state. |
| 80 | self.reset() |
| 81 | |
| 82 | def _match_applications(self): |
| 83 | """match the applications from the profile with applications on the card""" |
| 84 | apps_profile = self.profile.applications |
| 85 | |
| 86 | # When the profile does not feature any applications, then we are done already |
| 87 | if not apps_profile: |
| 88 | return [] |
| 89 | |
| 90 | # Read AIDs from card and match them against the applications defined by the |
| 91 | # card profile |
| 92 | aids_card = self.card.read_aids() |
| 93 | apps_taken = [] |
| 94 | if aids_card: |
| 95 | aids_taken = [] |
| 96 | print("AIDs on card:") |
| 97 | for a in aids_card: |
| 98 | for f in apps_profile: |
| 99 | if f.aid in a: |
| 100 | print(" %s: %s (EF.DIR)" % (f.name, a)) |
| 101 | aids_taken.append(a) |
| 102 | apps_taken.append(f) |
| 103 | aids_unknown = set(aids_card) - set(aids_taken) |
| 104 | for a in aids_unknown: |
| 105 | print(" unknown: %s (EF.DIR)" % a) |
| 106 | else: |
| 107 | print("warning: EF.DIR seems to be empty!") |
| 108 | |
| 109 | # Some card applications may not be registered in EF.DIR, we will actively |
| 110 | # probe for those applications |
Philipp Maier | d62182c | 2023-08-01 15:23:19 +0200 | [diff] [blame] | 111 | for f in sorted(set(apps_profile) - set(apps_taken), key=str): |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 112 | try: |
Philipp Maier | e30456b | 2023-12-07 11:07:55 +0100 | [diff] [blame] | 113 | # we can not use the lchan provided methods select, or select_file |
| 114 | # since those method work on an already finished file model. At |
| 115 | # this point we are still in the initialization process, so it is |
| 116 | # no problem when we access the card object directly without caring |
| 117 | # about updating other states. For normal selects at runtime, the |
| 118 | # caller must use the lchan provided methods select or select_file! |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 119 | data, sw = self.card.select_adf_by_aid(f.aid) |
Philipp Maier | 578cf12 | 2023-10-25 18:02:41 +0200 | [diff] [blame] | 120 | self.selected_adf = f |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 121 | if sw == "9000": |
| 122 | print(" %s: %s" % (f.name, f.aid)) |
| 123 | apps_taken.append(f) |
| 124 | except (SwMatchError, ProtocolError): |
| 125 | pass |
| 126 | return apps_taken |
| 127 | |
| 128 | def reset(self, cmd_app=None) -> Hexstr: |
| 129 | """Perform physical card reset and obtain ATR. |
| 130 | Args: |
| 131 | cmd_app : Command Application State (for unregistering old file commands) |
| 132 | """ |
| 133 | # delete all lchan != 0 (basic lchan) |
Philipp Maier | 92b9356 | 2023-07-21 11:38:26 +0200 | [diff] [blame] | 134 | for lchan_nr in list(self.lchan.keys()): |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 135 | if lchan_nr == 0: |
| 136 | continue |
| 137 | del self.lchan[lchan_nr] |
| 138 | atr = i2h(self.card.reset()) |
| 139 | # select MF to reset internal state and to verify card really works |
| 140 | self.lchan[0].select('MF', cmd_app) |
| 141 | self.lchan[0].selected_adf = None |
| 142 | return atr |
| 143 | |
| 144 | def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan': |
| 145 | """Add a logical channel to the runtime state. You shouldn't call this |
| 146 | directly but always go through RuntimeLchan.add_lchan().""" |
| 147 | if lchan_nr in self.lchan.keys(): |
| 148 | raise ValueError('Cannot create already-existing lchan %d' % lchan_nr) |
| 149 | self.lchan[lchan_nr] = RuntimeLchan(lchan_nr, self) |
| 150 | return self.lchan[lchan_nr] |
| 151 | |
| 152 | def del_lchan(self, lchan_nr: int): |
| 153 | if lchan_nr in self.lchan.keys(): |
| 154 | del self.lchan[lchan_nr] |
| 155 | return True |
| 156 | else: |
| 157 | return False |
| 158 | |
| 159 | def get_lchan_by_cla(self, cla) -> Optional['RuntimeLchan']: |
| 160 | lchan_nr = lchan_nr_from_cla(cla) |
| 161 | if lchan_nr in self.lchan.keys(): |
| 162 | return self.lchan[lchan_nr] |
| 163 | else: |
| 164 | return None |
| 165 | |
| 166 | |
| 167 | class RuntimeLchan: |
| 168 | """Represent the runtime state of a logical channel with a card.""" |
| 169 | |
| 170 | def __init__(self, lchan_nr: int, rs: RuntimeState): |
| 171 | self.lchan_nr = lchan_nr |
| 172 | self.rs = rs |
Philipp Maier | c038ccc | 2023-12-07 11:12:08 +0100 | [diff] [blame] | 173 | self.scc = self.rs.card._scc.fork_lchan(lchan_nr) |
| 174 | |
| 175 | # File reference data |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 176 | self.selected_file = self.rs.mf |
| 177 | self.selected_adf = None |
| 178 | self.selected_file_fcp = None |
| 179 | self.selected_file_fcp_hex = None |
| 180 | |
| 181 | def add_lchan(self, lchan_nr: int) -> 'RuntimeLchan': |
| 182 | """Add a new logical channel from the current logical channel. Just affects |
| 183 | internal state, doesn't actually open a channel with the UICC.""" |
| 184 | new_lchan = self.rs.add_lchan(lchan_nr) |
| 185 | # See TS 102 221 Table 8.3 |
| 186 | if self.lchan_nr != 0: |
| 187 | new_lchan.selected_file = self.get_cwd() |
| 188 | new_lchan.selected_adf = self.selected_adf |
| 189 | return new_lchan |
| 190 | |
| 191 | def selected_file_descriptor_byte(self) -> dict: |
| 192 | return self.selected_file_fcp['file_descriptor']['file_descriptor_byte'] |
| 193 | |
| 194 | def selected_file_shareable(self) -> bool: |
| 195 | return self.selected_file_descriptor_byte()['shareable'] |
| 196 | |
| 197 | def selected_file_structure(self) -> str: |
| 198 | return self.selected_file_descriptor_byte()['structure'] |
| 199 | |
| 200 | def selected_file_type(self) -> str: |
| 201 | return self.selected_file_descriptor_byte()['file_type'] |
| 202 | |
| 203 | def selected_file_num_of_rec(self) -> Optional[int]: |
| 204 | return self.selected_file_fcp['file_descriptor'].get('num_of_rec') |
| 205 | |
| 206 | def get_cwd(self) -> CardDF: |
| 207 | """Obtain the current working directory. |
| 208 | |
| 209 | Returns: |
| 210 | CardDF instance |
| 211 | """ |
| 212 | if isinstance(self.selected_file, CardDF): |
| 213 | return self.selected_file |
| 214 | else: |
| 215 | return self.selected_file.parent |
| 216 | |
| 217 | def get_application_df(self) -> Optional[CardADF]: |
| 218 | """Obtain the currently selected application DF (if any). |
| 219 | |
| 220 | Returns: |
| 221 | CardADF() instance or None""" |
| 222 | # iterate upwards from selected file; check if any is an ADF |
| 223 | node = self.selected_file |
| 224 | while node.parent != node: |
| 225 | if isinstance(node, CardADF): |
| 226 | return node |
| 227 | node = node.parent |
| 228 | return None |
| 229 | |
| 230 | def interpret_sw(self, sw: str): |
| 231 | """Interpret a given status word relative to the currently selected application |
| 232 | or the underlying card profile. |
| 233 | |
| 234 | Args: |
| 235 | sw : Status word as string of 4 hex digits |
| 236 | |
| 237 | Returns: |
| 238 | Tuple of two strings |
| 239 | """ |
| 240 | res = None |
| 241 | adf = self.get_application_df() |
| 242 | if adf: |
| 243 | app = adf.application |
| 244 | # The application either comes with its own interpret_sw |
| 245 | # method or we will use the interpret_sw method from the |
| 246 | # card profile. |
| 247 | if app and hasattr(app, "interpret_sw"): |
| 248 | res = app.interpret_sw(sw) |
| 249 | return res or self.rs.profile.interpret_sw(sw) |
| 250 | |
| 251 | def probe_file(self, fid: str, cmd_app=None): |
| 252 | """Blindly try to select a file and automatically add a matching file |
Philipp Maier | 1da8636 | 2023-10-31 13:17:14 +0100 | [diff] [blame] | 253 | object if the file actually exists.""" |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 254 | if not is_hex(fid, 4, 4): |
| 255 | raise ValueError( |
| 256 | "Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid) |
| 257 | |
| 258 | try: |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 259 | (data, sw) = self.scc.select_file(fid) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 260 | except SwMatchError as swm: |
| 261 | k = self.interpret_sw(swm.sw_actual) |
| 262 | if not k: |
| 263 | raise(swm) |
| 264 | raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) |
| 265 | |
| 266 | select_resp = self.selected_file.decode_select_response(data) |
| 267 | if (select_resp['file_descriptor']['file_descriptor_byte']['file_type'] == 'df'): |
| 268 | f = CardDF(fid=fid, sfid=None, name="DF." + str(fid).upper(), |
| 269 | desc="dedicated file, manually added at runtime") |
| 270 | else: |
| 271 | if (select_resp['file_descriptor']['file_descriptor_byte']['structure'] == 'transparent'): |
| 272 | f = TransparentEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), |
| 273 | desc="elementary file, manually added at runtime") |
| 274 | else: |
| 275 | f = LinFixedEF(fid=fid, sfid=None, name="EF." + str(fid).upper(), |
| 276 | desc="elementary file, manually added at runtime") |
| 277 | |
| 278 | self.selected_file.add_files([f]) |
| 279 | self.selected_file = f |
| 280 | return select_resp, data |
| 281 | |
| 282 | def _select_pre(self, cmd_app): |
| 283 | # unregister commands of old file |
| 284 | if cmd_app and self.selected_file.shell_commands: |
| 285 | for c in self.selected_file.shell_commands: |
| 286 | cmd_app.unregister_command_set(c) |
| 287 | |
| 288 | def _select_post(self, cmd_app): |
| 289 | # register commands of new file |
| 290 | if cmd_app and self.selected_file.shell_commands: |
| 291 | for c in self.selected_file.shell_commands: |
| 292 | cmd_app.register_command_set(c) |
| 293 | |
| 294 | def select_file(self, file: CardFile, cmd_app=None): |
| 295 | """Select a file (EF, DF, ADF, MF, ...). |
| 296 | |
| 297 | Args: |
| 298 | file : CardFile [or derived class] instance |
| 299 | cmd_app : Command Application State (for unregistering old file commands) |
| 300 | """ |
| 301 | # we need to find a path from our self.selected_file to the destination |
| 302 | inter_path = self.selected_file.build_select_path_to(file) |
| 303 | if not inter_path: |
| 304 | raise RuntimeError('Cannot determine path from %s to %s' % (self.selected_file, file)) |
| 305 | |
| 306 | self._select_pre(cmd_app) |
| 307 | |
| 308 | for p in inter_path: |
| 309 | try: |
| 310 | if isinstance(p, CardADF): |
Harald Welte | 6dd6f3e | 2023-10-22 10:28:18 +0200 | [diff] [blame] | 311 | (data, sw) = self.rs.card.select_adf_by_aid(p.aid, scc=self.scc) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 312 | self.selected_adf = p |
| 313 | else: |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 314 | (data, sw) = self.scc.select_file(p.fid) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 315 | self.selected_file = p |
| 316 | except SwMatchError as swm: |
| 317 | self._select_post(cmd_app) |
| 318 | raise(swm) |
| 319 | |
| 320 | self._select_post(cmd_app) |
| 321 | |
| 322 | def select(self, name: str, cmd_app=None): |
| 323 | """Select a file (EF, DF, ADF, MF, ...). |
| 324 | |
| 325 | Args: |
| 326 | name : Name of file to select |
| 327 | cmd_app : Command Application State (for unregistering old file commands) |
| 328 | """ |
| 329 | # handling of entire paths with multiple directories/elements |
| 330 | if '/' in name: |
| 331 | prev_sel_file = self.selected_file |
| 332 | pathlist = name.split('/') |
| 333 | # treat /DF.GSM/foo like MF/DF.GSM/foo |
| 334 | if pathlist[0] == '': |
| 335 | pathlist[0] = 'MF' |
| 336 | try: |
| 337 | for p in pathlist: |
| 338 | self.select(p, cmd_app) |
| 339 | return |
| 340 | except Exception as e: |
| 341 | # if any intermediate step fails, go back to where we were |
| 342 | self.select_file(prev_sel_file, cmd_app) |
| 343 | raise e |
| 344 | |
| 345 | sels = self.selected_file.get_selectables() |
| 346 | if is_hex(name): |
| 347 | name = name.lower() |
| 348 | |
| 349 | self._select_pre(cmd_app) |
| 350 | |
| 351 | if name in sels: |
| 352 | f = sels[name] |
| 353 | try: |
| 354 | if isinstance(f, CardADF): |
Harald Welte | 6dd6f3e | 2023-10-22 10:28:18 +0200 | [diff] [blame] | 355 | (data, sw) = self.rs.card.select_adf_by_aid(f.aid, scc=self.scc) |
Philipp Maier | 578cf12 | 2023-10-25 18:02:41 +0200 | [diff] [blame] | 356 | self.selected_adf = f |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 357 | else: |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 358 | (data, sw) = self.scc.select_file(f.fid) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 359 | self.selected_file = f |
| 360 | except SwMatchError as swm: |
| 361 | k = self.interpret_sw(swm.sw_actual) |
| 362 | if not k: |
| 363 | raise(swm) |
| 364 | raise RuntimeError("%s: %s - %s" % (swm.sw_actual, k[0], k[1])) |
| 365 | select_resp = f.decode_select_response(data) |
| 366 | else: |
| 367 | (select_resp, data) = self.probe_file(name, cmd_app) |
| 368 | |
| 369 | # store the raw + decoded FCP for later reference |
| 370 | self.selected_file_fcp_hex = data |
| 371 | self.selected_file_fcp = select_resp |
| 372 | |
| 373 | self._select_post(cmd_app) |
| 374 | return select_resp |
| 375 | |
| 376 | def status(self): |
| 377 | """Request STATUS (current selected file FCP) from card.""" |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 378 | (data, sw) = self.scc.status() |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 379 | return self.selected_file.decode_select_response(data) |
| 380 | |
| 381 | def get_file_for_selectable(self, name: str): |
| 382 | sels = self.selected_file.get_selectables() |
| 383 | return sels[name] |
| 384 | |
| 385 | def activate_file(self, name: str): |
| 386 | """Request ACTIVATE FILE of specified file.""" |
| 387 | sels = self.selected_file.get_selectables() |
| 388 | f = sels[name] |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 389 | data, sw = self.scc.activate_file(f.fid) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 390 | return data, sw |
| 391 | |
| 392 | def read_binary(self, length: int = None, offset: int = 0): |
| 393 | """Read [part of] a transparent EF binary data. |
| 394 | |
| 395 | Args: |
| 396 | length : Amount of data to read (None: as much as possible) |
| 397 | offset : Offset into the file from which to read 'length' bytes |
| 398 | Returns: |
| 399 | binary data read from the file |
| 400 | """ |
| 401 | if not isinstance(self.selected_file, TransparentEF): |
| 402 | raise TypeError("Only works with TransparentEF") |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 403 | return self.scc.read_binary(self.selected_file.fid, length, offset) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 404 | |
| 405 | def read_binary_dec(self) -> Tuple[dict, str]: |
| 406 | """Read [part of] a transparent EF binary data and decode it. |
| 407 | |
| 408 | Args: |
| 409 | length : Amount of data to read (None: as much as possible) |
| 410 | offset : Offset into the file from which to read 'length' bytes |
| 411 | Returns: |
| 412 | abstract decode data read from the file |
| 413 | """ |
| 414 | (data, sw) = self.read_binary() |
| 415 | dec_data = self.selected_file.decode_hex(data) |
| 416 | return (dec_data, sw) |
| 417 | |
| 418 | def update_binary(self, data_hex: str, offset: int = 0): |
| 419 | """Update transparent EF binary data. |
| 420 | |
| 421 | Args: |
| 422 | data_hex : hex string of data to be written |
| 423 | offset : Offset into the file from which to write 'data_hex' |
| 424 | """ |
| 425 | if not isinstance(self.selected_file, TransparentEF): |
| 426 | raise TypeError("Only works with TransparentEF") |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 427 | return self.scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.rs.conserve_write) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 428 | |
| 429 | def update_binary_dec(self, data: dict): |
| 430 | """Update transparent EF from abstract data. Encodes the data to binary and |
| 431 | then updates the EF with it. |
| 432 | |
| 433 | Args: |
| 434 | data : abstract data which is to be encoded and written |
| 435 | """ |
| 436 | data_hex = self.selected_file.encode_hex(data) |
| 437 | return self.update_binary(data_hex) |
| 438 | |
| 439 | def read_record(self, rec_nr: int = 0): |
| 440 | """Read a record as binary data. |
| 441 | |
| 442 | Args: |
| 443 | rec_nr : Record number to read |
| 444 | Returns: |
| 445 | hex string of binary data contained in record |
| 446 | """ |
| 447 | if not isinstance(self.selected_file, LinFixedEF): |
| 448 | raise TypeError("Only works with Linear Fixed EF") |
| 449 | # returns a string of hex nibbles |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 450 | return self.scc.read_record(self.selected_file.fid, rec_nr) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 451 | |
| 452 | def read_record_dec(self, rec_nr: int = 0) -> Tuple[dict, str]: |
| 453 | """Read a record and decode it to abstract data. |
| 454 | |
| 455 | Args: |
| 456 | rec_nr : Record number to read |
| 457 | Returns: |
| 458 | abstract data contained in record |
| 459 | """ |
| 460 | (data, sw) = self.read_record(rec_nr) |
| 461 | return (self.selected_file.decode_record_hex(data, rec_nr), sw) |
| 462 | |
| 463 | def update_record(self, rec_nr: int, data_hex: str): |
| 464 | """Update a record with given binary data |
| 465 | |
| 466 | Args: |
| 467 | rec_nr : Record number to read |
| 468 | data_hex : Hex string binary data to be written |
| 469 | """ |
| 470 | if not isinstance(self.selected_file, LinFixedEF): |
| 471 | raise TypeError("Only works with Linear Fixed EF") |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 472 | return self.scc.update_record(self.selected_file.fid, rec_nr, data_hex, |
Philipp Maier | 37e57e0 | 2023-09-07 12:43:12 +0200 | [diff] [blame] | 473 | conserve=self.rs.conserve_write, |
| 474 | leftpad=self.selected_file.leftpad) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 475 | |
| 476 | def update_record_dec(self, rec_nr: int, data: dict): |
| 477 | """Update a record with given abstract data. Will encode abstract to binary data |
| 478 | and then write it to the given record on the card. |
| 479 | |
| 480 | Args: |
| 481 | rec_nr : Record number to read |
| 482 | data_hex : Abstract data to be written |
| 483 | """ |
| 484 | data_hex = self.selected_file.encode_record_hex(data, rec_nr) |
| 485 | return self.update_record(rec_nr, data_hex) |
| 486 | |
| 487 | def retrieve_data(self, tag: int = 0): |
| 488 | """Read a DO/TLV as binary data. |
| 489 | |
| 490 | Args: |
| 491 | tag : Tag of TLV/DO to read |
| 492 | Returns: |
| 493 | hex string of full BER-TLV DO including Tag and Length |
| 494 | """ |
| 495 | if not isinstance(self.selected_file, BerTlvEF): |
| 496 | raise TypeError("Only works with BER-TLV EF") |
| 497 | # returns a string of hex nibbles |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 498 | return self.scc.retrieve_data(self.selected_file.fid, tag) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 499 | |
| 500 | def retrieve_tags(self): |
| 501 | """Retrieve tags available on BER-TLV EF. |
| 502 | |
| 503 | Returns: |
| 504 | list of integer tags contained in EF |
| 505 | """ |
| 506 | if not isinstance(self.selected_file, BerTlvEF): |
| 507 | raise TypeError("Only works with BER-TLV EF") |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 508 | data, sw = self.scc.retrieve_data(self.selected_file.fid, 0x5c) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 509 | tag, length, value, remainder = bertlv_parse_one(h2b(data)) |
| 510 | return list(value) |
| 511 | |
| 512 | def set_data(self, tag: int, data_hex: str): |
| 513 | """Update a TLV/DO with given binary data |
| 514 | |
| 515 | Args: |
| 516 | tag : Tag of TLV/DO to be written |
| 517 | data_hex : Hex string binary data to be written (value portion) |
| 518 | """ |
| 519 | if not isinstance(self.selected_file, BerTlvEF): |
| 520 | raise TypeError("Only works with BER-TLV EF") |
Harald Welte | 4625512 | 2023-10-21 23:40:42 +0200 | [diff] [blame] | 521 | return self.scc.set_data(self.selected_file.fid, tag, data_hex, conserve=self.rs.conserve_write) |
Harald Welte | 531894d | 2023-07-11 19:11:11 +0200 | [diff] [blame] | 522 | |
| 523 | def unregister_cmds(self, cmd_app=None): |
| 524 | """Unregister all file specific commands.""" |
| 525 | if cmd_app and self.selected_file.shell_commands: |
| 526 | for c in self.selected_file.shell_commands: |
| 527 | cmd_app.unregister_command_set(c) |
| 528 | |
| 529 | |
| 530 | |