From 6ff6c272897c1c89472e66b337b0ca8cd8457d80 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Jun 2026 10:03:42 +0200 Subject: [PATCH 1/5] Populate demo passwords only on first storage init The POPULATE build flag seeded password1/2/3 in app_main() on every app start, with no guard, so they piled up as duplicates on each launch. init_storage() now reports whether it just freshly initialized the storage (magic not yet set); the demo passwords are created only in that case. The behaviour stays fully gated by the POPULATE compile flag. --- src/app_main.c | 23 ++++++++++++++++------- src/globals.c | 5 +++-- src/globals.h | 4 +++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app_main.c b/src/app_main.c index cbcd4b2..f93980b 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -34,6 +34,9 @@ #include "metadata.h" #include "password_typing.h" #include "ui.h" +#if defined(POPULATE) +#include "password.h" +#endif // POPULATE const internalStorage_t N_storage_real; app_state_t app_state; @@ -42,7 +45,7 @@ volatile unsigned int G_led_status; void app_main() { int input_len = 0; - init_storage(); + bool storage_freshly_initialized = init_storage(); memset(&app_state, 0, sizeof(app_state)); ui_idle(); @@ -51,12 +54,18 @@ void app_main() { app_state.output_len = 0; #if defined(POPULATE) -#include "password.h" - // removing 1 as `sizeof` will include the trailing null byte in the result (10) - // but this app stores password without this trailing null byte. - create_new_password("password1", sizeof("password1") - 1); - create_new_password("password2", sizeof("password2") - 1); - create_new_password("password3", sizeof("password3") - 1); + // Only seed the demo passwords on a fresh storage (first run), otherwise + // they would be appended again on every app start and pile up as + // duplicates. + if (storage_freshly_initialized) { + // removing 1 as `sizeof` will include the trailing null byte in the result (10) + // but this app stores password without this trailing null byte. + create_new_password("password1", sizeof("password1") - 1); + create_new_password("password2", sizeof("password2") - 1); + create_new_password("password3", sizeof("password3") - 1); + } +#else + UNUSED(storage_freshly_initialized); #endif // POPULATE for (;;) { diff --git a/src/globals.c b/src/globals.c index ad1fe39..136eadd 100644 --- a/src/globals.c +++ b/src/globals.c @@ -1,10 +1,10 @@ #include "globals.h" #include "options.h" -void init_storage() { +bool init_storage() { if (N_storage.magic == STORAGE_MAGIC) { // already initialized - return; + return false; } uint32_t tmp = STORAGE_MAGIC; nvm_write((void *) &N_storage.magic, (void *) &tmp, sizeof(uint32_t)); @@ -18,4 +18,5 @@ void init_storage() { nvm_write((void *) &N_storage.metadata_count, (void *) &tmp, sizeof(N_storage.metadata_count)); nvm_write((void *) N_storage.metadatas, (void *) &tmp, 2); init_charset_options(); + return true; } diff --git a/src/globals.h b/src/globals.h index 476e33c..0f35b8d 100644 --- a/src/globals.h +++ b/src/globals.h @@ -11,4 +11,6 @@ extern volatile unsigned int G_led_status; #define CLA 0xE0 #define N_storage (*(volatile internalStorage_t *) PIC(&N_storage_real)) -void init_storage(void); +// Returns true if the storage was freshly initialized (first run), false if it +// was already initialized on a previous boot. +bool init_storage(void); From edbe8437a872a4c51d49894e4ebeb11f21e6f719 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Jun 2026 10:04:52 +0200 Subject: [PATCH 2/5] wui: fix showSaveFilePicker SecurityError on backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The save dialog was opened only after the device exchange (dump_metadatas), by which time the click's transient user activation had expired, so showSaveFilePicker threw "Must be handling a user gesture". Split saveJSON into pickSaveTarget() — called first, synchronously from the click handler while the activation is still valid — and writeJSON(), called once the backup data is ready. Falls back to a Blob/anchor download when the File System Access API is unavailable or refuses. --- clients/wui/src/App.jsx | 50 +++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/clients/wui/src/App.jsx b/clients/wui/src/App.jsx index 0a3e7f5..d7f9007 100644 --- a/clients/wui/src/App.jsx +++ b/clients/wui/src/App.jsx @@ -26,38 +26,50 @@ import packageJson from "../package.json"; const passwords = new PasswordsManager(); listen((log) => console.log(log)); -async function saveJSON(payload, suggestedName) { - const text = JSON.stringify(payload, null, 4); - +// showSaveFilePicker() requires transient user activation (a recent gesture), +// so it must be called synchronously from the click handler — before the +// device exchange, which takes too long and would otherwise let the activation +// expire ("Must be handling a user gesture"). This picks the destination up +// front and returns a target the caller writes to once the data is ready. +// Falls back to a Blob/anchor download when the File System Access API is +// unavailable or refuses (returns { cancelled } if the user dismisses it). +async function pickSaveTarget(suggestedName) { if (window.showSaveFilePicker) { - let handle; try { - handle = await window.showSaveFilePicker({ + const handle = await window.showSaveFilePicker({ suggestedName, types: [ { description: "Passwords backup", accept: { "application/json": [".json"] } }, ], }); + return { handle, suggestedName }; } catch (error) { - if (error.name === "AbortError") return false; - throw error; + if (error.name === "AbortError") return { cancelled: true }; + // SecurityError / NotAllowedError / unsupported: fall back to anchor. } - const writable = await handle.createWritable(); + } + return { suggestedName }; +} + +async function writeJSON(target, payload) { + const text = JSON.stringify(payload, null, 4); + + if (target.handle) { + const writable = await target.handle.createWritable(); await writable.write(text); await writable.close(); - return true; + return; } const blob = new Blob([text], { type: "application/json;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = suggestedName; + a.download = target.suggestedName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - return true; } export default function App() { @@ -118,16 +130,20 @@ export default function App() { } async function onBackup() { + // Pick the destination first, while the click's user activation is still + // valid (the device exchange below is too slow for showSaveFilePicker). + const target = await pickSaveTarget("passwords_backup.json"); + if (target.cancelled) { + setNotice({ appearance: "info", title: "Backup cancelled" }); + return; + } + setBusy(true); setNotice({ appearance: "info", title: 'Approve "Transfer metadatas?" on your device' }); try { const payload = await passwords.dump_metadatas(); - const saved = await saveJSON(payload, "passwords_backup.json"); - setNotice( - saved - ? { appearance: "success", title: "Backup saved" } - : { appearance: "info", title: "Backup cancelled" } - ); + await writeJSON(target, payload); + setNotice({ appearance: "success", title: "Backup saved" }); } catch (error) { setNotice({ appearance: "error", title: "Backup failed", description: String(error) }); } finally { From 0e4c8e673f57e40b4b161d08de0fa60520d96e1b Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Jun 2026 10:19:05 +0200 Subject: [PATCH 3/5] Fix format specifiers for size_t arguments --- src/password_list.c | 6 +++--- src/ui/ui_passwords.c | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/password_list.c b/src/password_list.c index 4cd5b29..d7244d4 100644 --- a/src/password_list.c +++ b/src/password_list.c @@ -45,7 +45,7 @@ void password_list_reset() { size_t password_list_get_offset(const size_t index) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_get_offset] Index %ld out of bounds\n", index); + PRINTF("[password_list_get_offset] Index %lu out of bounds\n", index); return -1; } return passwordList.offsets[index]; @@ -57,7 +57,7 @@ size_t password_list_get_current_offset() { const char *password_list_get_password(const size_t index) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_get_password] Index %ld out of bounds\n", index); + PRINTF("[password_list_get_password] Index %lu out of bounds\n", index); return NULL; } return passwordList.passwords[index]; @@ -72,7 +72,7 @@ bool password_list_add_password(const size_t index, const char *const password, const size_t length) { if (index >= MAX_METADATA_COUNT) { - PRINTF("[password_list_add_password] Index %ld out of bounds\n", index); + PRINTF("[password_list_add_password] Index %lu out of bounds\n", index); return false; } passwordList.offsets[index] = offset; diff --git a/src/ui/ui_passwords.c b/src/ui/ui_passwords.c index 0109918..32d51c6 100644 --- a/src/ui/ui_passwords.c +++ b/src/ui/ui_passwords.c @@ -136,8 +136,8 @@ static void confirm_password_deletion(const size_t index) { all_passwords = true; snprintf(msgBuffer, sizeof(msgBuffer), - "Confirm the deletion\nof all passwords (%d)", - N_storage.metadata_count); + "Confirm the deletion\nof all passwords (%u)", + (uint32_t) N_storage.metadata_count); } else { // A single password all_passwords = false; From 91fd1a4d020ca9105b9645dd17edbad3247710fd Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Jun 2026 10:20:36 +0200 Subject: [PATCH 4/5] Bump version --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1c2a7b1..78b2099 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ APPNAME ="Passwords" # Application version APPVERSION_M=1 APPVERSION_N=3 -APPVERSION_P=2 +APPVERSION_P=3 APPVERSION=$(APPVERSION_M).$(APPVERSION_N).$(APPVERSION_P) # Application source files From ea04928e211c2de44bb7bb3d19b19d749878ec88 Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Thu, 18 Jun 2026 10:33:14 +0200 Subject: [PATCH 5/5] Improve version check by reading the makefile directly --- tests/functional/conftest.py | 10 ++++++++++ tests/functional/test_cmd.py | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 75e989f..c3009ff 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -2,6 +2,8 @@ # functions intentionally match module-level fixture names. Silence the noisy # pylint warning for the whole file rather than per-function. # pylint: disable=redefined-outer-name +from pathlib import Path +import re import pytest from ragger.backend import RaisePolicy, BackendInterface from ragger.navigator import Navigator @@ -40,3 +42,11 @@ def navigator(custom_backend, device, golden_run): yield CustomNanoNavigator(custom_backend, device, golden_run) else: yield CustomTouchNavigator(custom_backend, device, golden_run) + +@pytest.fixture(name="app_version") +def app_version_fixture() -> tuple[int, int, int]: + with open(Path(__file__).parent.parent.parent / "Makefile", encoding="utf-8") as f: + parsed = {} + for m in re.findall(r"^APPVERSION_(\w)\s*=\s*(\d*)$", f.read(), re.MULTILINE): + parsed[m[0]] = int(m[1]) + return (parsed["M"], parsed["N"], parsed["P"]) diff --git a/tests/functional/test_cmd.py b/tests/functional/test_cmd.py index 6e2478c..067e584 100644 --- a/tests/functional/test_cmd.py +++ b/tests/functional/test_cmd.py @@ -1,8 +1,9 @@ from passwordsManager_cmd import PasswordsManagerCommand -def test_app_info(cmd: PasswordsManagerCommand): - assert cmd.get_app_info() == ("Passwords", "1.3.2") +def test_app_info(cmd: PasswordsManagerCommand, app_version: tuple[int, int, int]): + vers_str = ".".join(map(str, app_version)) + assert cmd.get_app_info() == ("Passwords", vers_str) def test_app_config(cmd: PasswordsManagerCommand):