diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh index 60061e34..f83e6a53 100755 --- a/contrib/jenkins.sh +++ b/contrib/jenkins.sh @@ -14,6 +14,7 @@ virtualenv -p python3 venv --system-site-packages pip install pytlv pip install pyyaml pip install cmd2 +pip install jsonpath-ng # Execute automatically discovered unit tests first python -m unittest discover -v -s tests/ diff --git a/docs/shell.rst b/docs/shell.rst index 2edebbf3..3cfc849c 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -226,6 +226,33 @@ update_binary_decoded :module: pySim.filesystem :func: TransparentEF.ShellCommands.upd_bin_dec_parser +In normal operation, update_binary_decoded needs a JSON document representing the entire file contents as +input. This can be inconvenient if you want to keep 99% of the content but just toggle one specific +parameter. That's where the JSONpath support comes in handy: You can specify a JSONpath to an element +inside the document as well as a new value for tat field: + +Th below example demonstrates this by modifying the ofm field within EF.AD: + +:: + + pySIM-shell (MF/ADF.USIM/EF.AD)> read_binary_decoded + { + "ms_operation_mode": "normal", + "specific_facilities": { + "ofm": true + }, + "len_of_mnc_in_imsi": 2 + } + pySIM-shell (MF/ADF.USIM/EF.AD)> update_binary_decoded --json-path specific_facilities.ofm false + pySIM-shell (MF/ADF.USIM/EF.AD)> read_binary_decoded + { + "ms_operation_mode": "normal", + "specific_facilities": { + "ofm": false + }, + "len_of_mnc_in_imsi": 2 + } + cmd2 settable parameters diff --git a/pySim/filesystem.py b/pySim/filesystem.py index a65a7643..f8443f35 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -35,6 +35,7 @@ from typing import cast, Optional, Iterable, List, Any, Dict, Tuple from pySim.utils import sw_match, h2b, b2h, is_hex from pySim.exceptions import * +from pySim.jsonpath import js_path_find, js_path_modify class CardFile(object): """Base class for all objects in the smart card filesystem. @@ -418,10 +419,16 @@ class TransparentEF(CardEF): upd_bin_dec_parser = argparse.ArgumentParser() upd_bin_dec_parser.add_argument('data', help='Abstract data (JSON format) to write') + upd_bin_dec_parser.add_argument('--json-path', type=str, + help='JSON path to modify specific element of file only') @cmd2.with_argparser(upd_bin_dec_parser) def do_update_binary_decoded(self, opts): """Encode + Update (Write) data of a transparent EF""" - data_json = json.loads(opts.data) + if opts.json_path: + (data_json, sw) = self._cmd.rs.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) if data: self._cmd.poutput_json(data) @@ -574,10 +581,17 @@ class LinFixedEF(CardEF): upd_rec_dec_parser = argparse.ArgumentParser() upd_rec_dec_parser.add_argument('record_nr', type=int, help='Number of record to be read') upd_rec_dec_parser.add_argument('data', help='Data bytes (hex format) to write') + upd_rec_dec_parser.add_argument('--json-path', type=str, + help='JSON path to modify specific element of record only') @cmd2.with_argparser(upd_rec_dec_parser) def do_update_record_decoded(self, opts): """Encode + Update (write) data to a record-oriented EF""" - (data, sw) = self._cmd.rs.update_record_dec(opts.record_nr, opts.data) + if opts.json_path: + (data_json, sw) = self._cmd.rs.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(opts.record_nr, data_json) if data: self._cmd.poutput(data) diff --git a/pySim/jsonpath.py b/pySim/jsonpath.py new file mode 100644 index 00000000..98dbd75d --- /dev/null +++ b/pySim/jsonpath.py @@ -0,0 +1,48 @@ +# coding=utf-8 +import json +import pprint +import jsonpath_ng + +"""JSONpath utility functions as needed within pysim. + +As pySim-sell has the ability to represent SIM files as JSON strings, +adding JSONpath allows us to conveniently modify individual sub-fields +of a file or record in its JSON representation. +""" + +# (C) 2021 by Harald Welte +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +def js_path_find(js_dict, js_path): + """Find/Match a JSON path within a given JSON-serializable dict. + Args: + js_dict : JSON-serializable dict to operate on + js_path : JSONpath string + Returns: Result of the JSONpath expression + """ + jsonpath_expr = jsonpath_ng.parse(js_path) + return jsonpath_expr.find(js_dict) + +def js_path_modify(js_dict, js_path, new_val): + """Find/Match a JSON path within a given JSON-serializable dict. + Args: + js_dict : JSON-serializable dict to operate on + js_path : JSONpath string + new_val : New value for field in js_dict at js_path + """ + jsonpath_expr = jsonpath_ng.parse(js_path) + jsonpath_expr.find(js_dict) + jsonpath_expr.update(js_dict, new_val) + diff --git a/requirements.txt b/requirements.txt index 978a3dbc..f203ed18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ pyscard serial pytlv cmd2 +jsonpath-ng diff --git a/setup.py b/setup.py index d9f742c8..a02e3270 100644 --- a/setup.py +++ b/setup.py @@ -13,6 +13,7 @@ setup( "serial", "pytlv", "cmd2" + "jsonpath-ng" ], scripts=[ 'pySim-prog.py',