diff --git a/releases/Next-ChangeLog.md b/releases/Next-ChangeLog.md index ecb72964..b5ea1180 100644 --- a/releases/Next-ChangeLog.md +++ b/releases/Next-ChangeLog.md @@ -8,6 +8,7 @@ This lists the new changes that have not yet been published in a normal release. - Enhancement: Add warning for zero value outputs if not `OP_RETURN` - Enhancement: Show QR codes of output addresses in transaction output explorer. Explorer is now offered for transactions of all sizes, not just complex ones. + Enhancement: Add ability to rename files on SD card via `Advanced/Tools -> File Management -> List Files` - Bugfix: If all change outputs have `nValue=0` they were not shown in UX. - Bugfix: Disallow negative input/output amounts in PSBT. - Bugfix: Fix filesystem initialization after Wife LFS or Destroy Seed. diff --git a/shared/actions.py b/shared/actions.py index f64fe189..7701cb51 100644 --- a/shared/actions.py +++ b/shared/actions.py @@ -1683,30 +1683,39 @@ async def list_files(*A): from pincodes import pa digest = chk.digest() - basename = fn.rsplit('/', 1)[-1] - msg_base = 'SHA256(%s)\n\n%s\n\nPress ' % (basename, B2A(digest)) - escape = "6" + path, basename = fn.rsplit('/', 1) + msg_base = 'SHA256(%s)\n\n' + B2A(digest) + '\n\nPress (1) to rename file, ' + escape = "61" if pa.has_secrets(): - msg_sign = '(4) to sign file digest and export detached signature, ' + msg_base += '(4) to sign file digest and export detached signature, ' escape += "4" - else: - msg_sign = "" - msg_delete = '(6) to delete.' - msg = msg_base + msg_sign + msg_delete + msg_base += '(6) to delete.' + while True: - ch = await ux_show_story(msg, escape=escape) + ch = await ux_show_story(msg_base % basename, escape=escape) if ch == "x": break - if ch in '46': + if ch in '461': with CardSlot() as card: if ch == '6': card.securely_blank_file(fn) break + elif ch == '1': + new_basename = await ux_input_text(basename, max_len=32, min_len=3) + if new_basename: + try: + # prohibit both slashes and space in filenames + for s in "\/ ": + assert s not in new_basename, "illegal char" + uos.rename(path + "/" + basename, path + "/" + new_basename) + basename = new_basename + except Exception as e: + await ux_show_story("Failed to rename the file. " + str(e), + title="Failure") else: from msgsign import write_sig_file sig_nice = write_sig_file([(digest, fn)]) await ux_show_story("Signature file %s written." % sig_nice) - msg = msg_base + msg_delete return async def file_picker(suffix=None, min_size=1, max_size=1000000, taster=None, diff --git a/testing/test_ux.py b/testing/test_ux.py index e0cfcc70..afb1eeac 100644 --- a/testing/test_ux.py +++ b/testing/test_ux.py @@ -2,7 +2,7 @@ # import pytest, time, os, re, hashlib, shutil from helpers import xfp2str, prandom -from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE +from charcodes import KEY_DOWN, KEY_QR, KEY_NFC, KEY_DELETE, KEY_CANCEL from constants import AF_CLASSIC, simulator_fixed_words, simulator_fixed_xfp from mnemonic import Mnemonic from bip32 import BIP32Node @@ -818,7 +818,6 @@ def test_sign_file_from_list_files(f_len, goto_home, cap_story, pick_menu_item, verify_detached_signature_file([fname], signame, "sd", AF_CLASSIC) time.sleep(0.1) _, story = cap_story() - assert "(4) to sign file digest and export detached signature" not in story assert "(6) to delete" in story @@ -828,6 +827,68 @@ def test_sign_file_from_list_files(f_len, goto_home, cap_story, pick_menu_item, assert "List Files" in menu +def test_rename_from_list_files(goto_home, cap_story, pick_menu_item, need_keypress, is_q1, + microsd_path, press_select, cap_screen, enter_complex): + def clear(fname): + for i in range(len(fname)): + if not is_q1 and not i: + # Mk4 different menu entry UX + continue + need_keypress(KEY_DELETE if is_q1 else "x") + time.sleep(0.01) + + fname = "file_to_rename.pdf" + fpath = microsd_path(fname) + contents = os.urandom(64) + digest = hashlib.sha256(contents).digest().hex() + with open(fpath, "wb") as f: + f.write(contents) + + goto_home() + pick_menu_item("Advanced/Tools") + pick_menu_item('File Management') + pick_menu_item('List Files') + time.sleep(0.1) + pick_menu_item(fname) + time.sleep(0.1) + _, story = cap_story() + assert f"SHA256({fname})" in story + assert digest in story + assert "Press (1) to rename file" in story + need_keypress("1") + time.sleep(0.1) + if is_q1: + scr = cap_screen() + assert fname in scr + + clear(fname) + + bad_fnames = ["renamed file.txt", "/sd/renamed_file.txt", "renamed\\file.txt"] + for bad in bad_fnames: + enter_complex(bad, b39pass=False) + time.sleep(.1) + title, story = cap_story() + assert title == "Failure" + assert "Failed to rename the file" in story + assert "illegal char" in story + press_select() + time.sleep(.1) + need_keypress("1") # rename again + time.sleep(.1) + clear(fname) + if not is_q1: + need_keypress("1") # toggle case back to upper (enter complex expect to start in that state) + + new_fname = "renamed_file.txt" + enter_complex(new_fname, b39pass=False) + time.sleep(.1) + _, story = cap_story() + assert f"SHA256({new_fname})" in story + assert digest in story + assert not os.path.exists(fpath) + assert os.path.exists(microsd_path(new_fname)) + + def test_bip39_pw_signing_xfp_ux(pick_menu_item, press_select, cap_story, enter_complex, reset_seed_words, cap_menu, go_to_passphrase, microsd_wipe): microsd_wipe() # need to wipe all PSBT on SD card so we do not proceed to signing