filesystem: edit_{binary,record}_decoded: add encode/decode examples

When invoking `edit_binary_decoded` or `edit_record_decoded`, the
temp file opened in the editor now contains the EF's encode/decode
test vectors as //-comment lines below the JSON content, similar to
how 'git commit' appends comments to the commit message template.
The comment block is stripped before JSON parsing on save,
so it has no effect on the written data.

The feature is implemented via a new module-level JsonEditor context
manager class that encapsulates the full edit cycle:

* write JSON + examples to a TemporaryDirectory
* invoke the editor
* read back, strip //-comments, parse and return the result

Change-Id: I5a046a9c7ba7e08a98cf643d5a26bc669539b38f
Related: OS#6900
This commit is contained in:
Vadim Yanitskiy
2026-03-31 05:04:29 +07:00
committed by laforge
parent fc932a2ee9
commit 5e2fd148f8

View File

@@ -552,6 +552,74 @@ class CardADF(CardDF):
return lchan.selected_file.application.export(as_json, lchan) return lchan.selected_file.application.export(as_json, lchan)
class JsonEditor:
"""Context manager for editing a JSON-encoded EF value in an external editor.
Writes the current JSON value (plus encode/decode examples as //-comments)
to a temporary file, opens the user's editor, then reads the result back
(stripping comment lines) and returns it as the context variable::
with JsonEditor(self._cmd, orig_json, ef) as edited_json:
if edited_json != orig_json:
...write back...
"""
def __init__(self, cmd, orig_json, ef):
self._cmd = cmd
self._orig_json = orig_json
self._ef = ef
self._tmpdir = None
@staticmethod
def _strip_comments(text: str) -> str:
"""Strip //-comment lines from text before JSON parsing."""
# TODO: also strip inline comments?
return '\n'.join(line for line in text.splitlines() if not line.lstrip().startswith('//'))
def _append_examples_as_comments(self, text_file) -> None:
"""Append encode/decode test vectors as //-comment lines to an open file.
The examples are taken from _test_de_encode and _test_decode class
attributes (same source as the auto-generated filesystem documentation).
The comment block is intentionally ignored on read-back by _strip_comments."""
vectors = []
for attr in ('_test_de_encode', '_test_decode'):
v = getattr(type(self._ef), attr, None)
if v:
vectors.extend(v)
if not vectors:
return
ef = self._ef
parts = [ef.fully_qualified_path_str()]
if ef.fid:
parts.append(f'({ef.fid.upper()})')
if ef.desc:
parts.append(f'- {ef.desc}')
text_file.write(f'\n\n// {" ".join(parts)}\n')
text_file.write('// Examples (ignored on save):\n')
for t in vectors:
if len(t) >= 3:
encoded, record_nr, decoded = t[0], t[1], t[2]
text_file.write(f'// record {record_nr}: {encoded}\n')
else:
encoded, decoded = t[0], t[1]
text_file.write(f'// file: {encoded}\n')
for line in json.dumps(decoded, indent=4, cls=JsonEncoder).splitlines():
text_file.write(f'// {line}\n')
def __enter__(self) -> object:
"""Write JSON + examples to a temp file, run the editor, return parsed result."""
self._tmpdir = tempfile.TemporaryDirectory(prefix='pysim_')
filename = '%s/file' % self._tmpdir.name
with open(filename, 'w') as text_file:
json.dump(self._orig_json, text_file, indent=4, cls=JsonEncoder)
self._append_examples_as_comments(text_file)
self._cmd.run_editor(filename)
with open(filename, 'r') as text_file:
return json.loads(self._strip_comments(text_file.read()))
def __exit__(self, *args):
self._tmpdir.cleanup()
class CardEF(CardFile): class CardEF(CardFile):
"""EF (Entry File) in the smart card filesystem""" """EF (Entry File) in the smart card filesystem"""
@@ -657,15 +725,8 @@ class TransparentEF(CardEF):
def do_edit_binary_decoded(self, _opts): def do_edit_binary_decoded(self, _opts):
"""Edit the JSON representation of the EF contents in an editor.""" """Edit the JSON representation of the EF contents in an editor."""
(orig_json, _sw) = self._cmd.lchan.read_binary_dec() (orig_json, _sw) = self._cmd.lchan.read_binary_dec()
with tempfile.TemporaryDirectory(prefix='pysim_') as dirname: ef = self._cmd.lchan.selected_file
filename = '%s/file' % dirname with JsonEditor(self._cmd, orig_json, ef) as edited_json:
# write existing data as JSON to file
with open(filename, 'w') as text_file:
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
# run a text editor
self._cmd.run_editor(filename)
with open(filename, 'r') as text_file:
edited_json = json.load(text_file)
if edited_json == orig_json: if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write") self._cmd.poutput("Data not modified, skipping write")
else: else:
@@ -959,15 +1020,8 @@ class LinFixedEF(CardEF):
def do_edit_record_decoded(self, opts): def do_edit_record_decoded(self, opts):
"""Edit the JSON representation of one record in an editor.""" """Edit the JSON representation of one record in an editor."""
(orig_json, _sw) = self._cmd.lchan.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: ef = self._cmd.lchan.selected_file
filename = '%s/file' % dirname with JsonEditor(self._cmd, orig_json, ef) as edited_json:
# write existing data as JSON to file
with open(filename, 'w') as text_file:
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
# run a text editor
self._cmd.run_editor(filename)
with open(filename, 'r') as text_file:
edited_json = json.load(text_file)
if edited_json == orig_json: if edited_json == orig_json:
self._cmd.poutput("Data not modified, skipping write") self._cmd.poutput("Data not modified, skipping write")
else: else: