Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package otpclient for openSUSE:Factory 
checked in at 2026-07-14 13:54:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/otpclient (Old)
 and      /work/SRC/openSUSE:Factory/.otpclient.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "otpclient"

Tue Jul 14 13:54:28 2026 rev:51 rq:1365607 version:5.1.4

Changes:
--------
--- /work/SRC/openSUSE:Factory/otpclient/otpclient.changes      2026-06-30 
15:14:57.783151476 +0200
+++ /work/SRC/openSUSE:Factory/.otpclient.new.1991/otpclient.changes    
2026-07-14 13:57:01.701595758 +0200
@@ -1,0 +2,42 @@
+Fri Jul 10 10:02:17 UTC 2026 - Paolo Stivanin <[email protected]>
+
+- Update to 5.1.4:
+  * FIX: a database containing a token with an out-of-range digit
+    count or period refused to open, locking you out of all of your
+    tokens. The accepted ranges now match the OTP engine exactly
+    (digits 4 to 10, period 1 to 120 seconds), so such tokens load
+    again. Any token the engine still cannot use (out-of-range
+    value, invalid secret, unsupported algorithm) is set aside and
+    preserved in the database rather than blocking the whole load
+    (#464, follow-up to #458/#462)
+  * FIX: right-click context menus on tokens and databases were
+    clipped to the scrolled area and needed scrolling to reach
+    lower items such as "Set Group"; they now size to their
+    contents (#465)
+
+-------------------------------------------------------------------
+Thu Jul  9 12:18:09 UTC 2026 - Paolo Stivanin <[email protected]>
+
+- Update to 5.1.3:
+  * FIX: a database (or import) containing a token with neither an
+    account name nor an issuer refused to open on 5.1.x with "Token
+    N has neither a label nor an issuer", locking you out of all of
+    your tokens. Such tokens now load with a synthesized Unknown N
+    placeholder label instead of being rejected, so the database
+    opens and the token stays usable and editable. Interactive
+    manual entry and editing still require a name (#462, follow-up
+    to #458)
+
+-------------------------------------------------------------------
+Wed Jul  8 09:17:06 UTC 2026 - Paolo Stivanin <[email protected]>
+
+- Update to 5.1.2:
+  * FIX: screensaver, session lock, and suspend now respect the Auto-
+    Lock setting. With Auto-Lock off, the app no longer locks the
+    database on every screen lock and then leaves you behind a manual
+    password prompt. The mutual exclusivity between Auto-Lock and
+    Secret Service is also restored, enabling one greys out the
+    other, and a legacy profile with both enabled is reconciled on
+    startup (#460, re-report of #279)
+
+-------------------------------------------------------------------

Old:
----
  v5.1.1.tar.gz
  v5.1.1.tar.gz.asc

New:
----
  v5.1.4.tar.gz
  v5.1.4.tar.gz.asc

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ otpclient.spec ++++++
--- /var/tmp/diff_new_pack.E0btL7/_old  2026-07-14 13:57:02.569625476 +0200
+++ /var/tmp/diff_new_pack.E0btL7/_new  2026-07-14 13:57:02.573625613 +0200
@@ -18,7 +18,7 @@
 
 %define uclname OTPClient
 Name:           otpclient
-Version:        5.1.1
+Version:        5.1.4
 Release:        0
 Summary:        Simple GTK+ client for managing TOTP and HOTP
 License:        GPL-3.0-or-later

++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.E0btL7/_old  2026-07-14 13:57:02.641627941 +0200
+++ /var/tmp/diff_new_pack.E0btL7/_new  2026-07-14 13:57:02.649628214 +0200
@@ -1,5 +1,5 @@
-mtime: 1782805223
-commit: 5e0abec62dd086beb5a6c336598ffd44e909c3fbf58727be47043db47b3f1e4a
+mtime: 1783677767
+commit: 04838f01d636678220ee57cb23fc179016d264a8d2db38d44d75884e0a1996f0
 url: https://src.opensuse.org/GNOME/otpclient
 revision: factory
 

++++++ build.specials.obscpio ++++++

++++++ build.specials.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/.gitignore new/.gitignore
--- old/.gitignore      1970-01-01 01:00:00.000000000 +0100
+++ new/.gitignore      2026-07-10 12:02:47.000000000 +0200
@@ -0,0 +1,4 @@
+*.obscpio
+*.osc
+_build.*
+.pbuild


++++++ v5.1.1.tar.gz -> v5.1.4.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/CMakeLists.txt 
new/OTPClient-5.1.4/CMakeLists.txt
--- old/OTPClient-5.1.1/CMakeLists.txt  2026-06-30 09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/CMakeLists.txt  2026-07-10 11:41:32.000000000 +0200
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.25)
-project(OTPClient VERSION "5.1.1" LANGUAGES "C")
+project(OTPClient VERSION "5.1.4" LANGUAGES "C")
 include(GNUInstallDirs)
 include(CTest)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/OTPClient-5.1.1/data/com.github.paolostivanin.OTPClient.appdata.xml 
new/OTPClient-5.1.4/data/com.github.paolostivanin.OTPClient.appdata.xml
--- old/OTPClient-5.1.1/data/com.github.paolostivanin.OTPClient.appdata.xml     
2026-06-30 09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/data/com.github.paolostivanin.OTPClient.appdata.xml     
2026-07-10 11:41:32.000000000 +0200
@@ -26,7 +26,7 @@
       <li>desktop search provider for GNOME Shell and KDE KRunner (opt-in 
trigger keyword)</li>
       <li>command-line companion (otpclient-cli) with scriptable 
table/JSON/CSV output</li>
       <li>support for TOTP, HOTP, and Steam codes</li>
-      <li>configurable digits (4 to 10), period (10 to 120 seconds), and 
algorithm (SHA1, SHA256, SHA512)</li>
+      <li>configurable digits (4 to 10), period (1 to 120 seconds), and 
algorithm (SHA1, SHA256, SHA512)</li>
       <li>import and export of encrypted/plain Aegis backups</li>
       <li>import and export of encrypted/plain Authenticator Pro and 2FAS 
backups</li>
       <li>import and export of plain FreeOTP+ backups (key URI format)</li>
@@ -90,6 +90,31 @@
   </content_rating>
 
   <releases>
+    <release version="5.1.4" date="2026-07-10">
+      <description>
+        <p>Bug-fix release. Following the anonymous-token fix in 5.1.3, a 
database containing a token with an out-of-range digit count still refused to 
open, again locking users out of every other token. The load-time validator was 
stricter than the OTP engine and the app's own UI; the accepted ranges now 
match the engine (digits 4 to 10, period 1 to 120 seconds). Any token the 
engine still cannot use is set aside so the rest of the database opens, and is 
preserved for repair rather than dropped.</p>
+        <ul>
+          <li>FIX: a database containing a token with an out-of-range digit 
count (or period) refused to open, locking users out of all their tokens. 
Accepted ranges now match the OTP engine (digits 4 to 10, period 1 to 120), and 
any token that still cannot be loaded is kept aside and preserved instead of 
bricking the whole database (#464, follow-up to #458/#462)</li>
+          <li>FIX: right-click context menus on tokens and databases were 
clipped to the scrolled area and needed scrolling to reach lower items such as 
"Set Group"; they now size to their contents (#465)</li>
+        </ul>
+      </description>
+    </release>
+    <release version="5.1.3" date="2026-07-09">
+      <description>
+        <p>Bug-fix release. Following 5.1.1, which restored issuer-only 
tokens, a database containing a token with neither an account name nor an 
issuer still refused to open, and a single such token blocked every other token 
from loading. These anonymous tokens, common in databases created before 5.1.0 
added load-time validation, now load with a synthesized placeholder label 
instead of being rejected.</p>
+        <ul>
+          <li>FIX: a database (or import) containing a token with neither an 
account name nor an issuer refused to open on 5.1.x, locking users out of all 
their tokens. Such tokens now load with a synthesized "Unknown N" label; 
interactive manual entry and editing still require a name (#462, follow-up to 
#458)</li>
+        </ul>
+      </description>
+    </release>
+    <release version="5.1.2" date="2026-07-07">
+      <description>
+        <p>Bug-fix release. Screen and session locking, and locking on system 
suspend, now honor the Auto-Lock setting the same way the inactivity timer 
already did, so with Auto-Lock off the database is no longer locked on every 
screen lock. The mutual exclusivity between Auto-Lock and the Secret Service 
integration is restored.</p>
+        <ul>
+          <li>FIX: screensaver, session lock, and suspend now respect the 
Auto-Lock setting, so with Auto-Lock off the app no longer locks and then 
prompts for a password on every screen lock; Auto-Lock and Secret Service are 
mutually exclusive again, including reconciliation of a legacy both-on profile 
(#460, re-report of #279)</li>
+        </ul>
+      </description>
+    </release>
     <release version="5.1.1" date="2026-06-30">
       <description>
         <p>Bug-fix release. Databases containing a token with an issuer but no 
account name (for example some ProtonMail or Steam entries) failed to open on 
5.1.0 with "Could not open database: Token has a missing label", locking users 
out of the entire database. Such tokens are valid again.</p>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/cli/exec-action.c 
new/OTPClient-5.1.4/src/cli/exec-action.c
--- old/OTPClient-5.1.1/src/cli/exec-action.c   2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/cli/exec-action.c   2026-07-10 11:41:32.000000000 
+0200
@@ -211,6 +211,12 @@
             g_clear_error (&err);
             return FALSE;
         }
+        /* Issue #464: broken tokens are set aside so the database still 
opens. */
+        guint quarantined = db_get_quarantined_count (db_data);
+        if (quarantined > 0)
+            g_printerr (ngettext ("Warning: %u token could not be loaded and 
was skipped.\n",
+                                  "Warning: %u tokens could not be loaded and 
were skipped.\n",
+                                  quarantined), quarantined);
     }
 
     if (use_secret_service == TRUE && secret_service_runtime_ok &&
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/authpro.c 
new/OTPClient-5.1.4/src/common/authpro.c
--- old/OTPClient-5.1.1/src/common/authpro.c    2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/common/authpro.c    2026-07-10 11:41:32.000000000 
+0200
@@ -396,8 +396,9 @@
         otp->account_name = g_strdup (json_string_value (json_object_get (obj, 
"Username")));
         otp->secret = secure_strdup (secret_str);
         otp->digits = (guint32)json_integer_value (json_object_get(obj, 
"Digits"));
-        otp->counter = json_integer_value (json_object_get (obj, "Counter"));
-        otp->period = (guint32)json_integer_value (json_object_get (obj, 
"Period"));
+        /* period and counter share a union, so only the member matching the
+         * token type may be written; it is set per-type in the switch below.
+         * Writing both here clobbered the HOTP counter with the Period. */
 
         gboolean skip = FALSE;
         guint32 algo = (guint32)json_integer_value (json_object_get(obj, 
"Algorithm"));
@@ -421,12 +422,15 @@
         switch (type) {
             case 1:
                 otp->type = g_strdup ("HOTP");
+                otp->counter = json_integer_value (json_object_get (obj, 
"Counter"));
                 break;
             case 2:
                 otp->type = g_strdup ("TOTP");
+                otp->period = (guint32)json_integer_value (json_object_get 
(obj, "Period"));
                 break;
             case 4:
                 otp->type = g_strdup ("TOTP");
+                otp->period = (guint32)json_integer_value (json_object_get 
(obj, "Period"));
                 g_free (otp->issuer);
                 otp->issuer = g_strdup ("Steam");
                 break;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/db-common.c 
new/OTPClient-5.1.4/src/common/db-common.c
--- old/OTPClient-5.1.1/src/common/db-common.c  2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/common/db-common.c  2026-07-10 11:41:32.000000000 
+0200
@@ -112,6 +112,9 @@
 
 static void      restore_live_from_committed (DatabaseData *db_data);
 
+static gboolean  partition_valid_tokens (DatabaseData     *db_data,
+                                         GError          **err);
+
 
 void
 db_invalidate_kdf_cache (DatabaseData *db_data)
@@ -149,6 +152,10 @@
         json_decref (db_data->committed_json_data);
         db_data->committed_json_data = NULL;
     }
+    if (db_data->quarantined_tokens != NULL) {
+        json_decref (db_data->quarantined_tokens);
+        db_data->quarantined_tokens = NULL;
+    }
 
     g_slist_free_full (db_data->data_to_add, (GDestroyNotify) json_decref);
     db_data->data_to_add = NULL;
@@ -392,6 +399,43 @@
 }
 
 
+guint
+db_get_quarantined_count (DatabaseData *db_data)
+{
+    if (db_data == NULL || db_data->quarantined_tokens == NULL)
+        return 0;
+    return (guint) json_array_size (db_data->quarantined_tokens);
+}
+
+
+/* Load-time policy: never let one bad token brick the whole database. Move 
every
+ * token that fails validation into db_data->quarantined_tokens (preserved and
+ * re-merged on save, surfaced in the UI for repair) so the remaining valid
+ * tokens open normally. Still fails hard when the root is not a JSON array 
(the
+ * file is structurally corrupt rather than merely holding a bad entry). 
Rebuilds
+ * quarantined_tokens from scratch each call so the migration re-decrypt pass
+ * does not double-count. */
+static gboolean
+partition_valid_tokens (DatabaseData  *db_data,
+                        GError       **err)
+{
+    if (!json_is_array (db_data->in_memory_json_data)) {
+        g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
+                     "Database JSON root must be an array.");
+        return FALSE;
+    }
+    if (db_data->quarantined_tokens != NULL)
+        json_decref (db_data->quarantined_tokens);
+    db_data->quarantined_tokens = json_array ();
+    guint set_aside = otp_extract_invalid_tokens (db_data->in_memory_json_data,
+                                                  db_data->quarantined_tokens);
+    if (set_aside > 0)
+        g_info ("Set aside %u token(s) that failed validation; opened the "
+                "database with the remaining valid tokens.", set_aside);
+    return TRUE;
+}
+
+
 void
 load_db (DatabaseData    *db_data,
          GError         **err)
@@ -432,7 +476,13 @@
                      "Error while loading json data: %s", jerr.text);
         return;
     }
-    if (!otp_validate_database_root (db_data->in_memory_json_data, err))
+    /* Give any anonymous token (neither label nor issuer) a placeholder so a
+     * single such entry does not make the whole database refuse to open
+     * (issue #462). The names live in memory and persist on the next save. */
+    guint repaired = otp_repair_database_root (db_data->in_memory_json_data);
+    if (repaired > 0)
+        g_info ("Assigned placeholder labels to %u anonymous token(s) on 
load.", repaired);
+    if (!partition_valid_tokens (db_data, err))
         return;
 
     if (db_data->current_db_version < DB_VERSION || 
db_data->needs_legacy_kdf_migration) {
@@ -459,7 +509,8 @@
                          "Error while loading json data: %s", jerr.text);
             return;
         }
-        if (!otp_validate_database_root (db_data->in_memory_json_data, err))
+        otp_repair_database_root (db_data->in_memory_json_data);
+        if (!partition_valid_tokens (db_data, err))
             return;
     }
 
@@ -726,9 +777,13 @@
     OtpImportReport *report = ctx->report != NULL ? ctx->report : &local;
     *report = (OtpImportReport) {0, 0, 0};
 
-    for (GSList *l = ctx->otps; l != NULL; l = l->next) {
+    gsize import_index = 0;
+    for (GSList *l = ctx->otps; l != NULL; l = l->next, import_index++) {
         otp_t *otp = l->data;
         GError *validation_err = NULL;
+        /* Keep an anonymous imported token instead of dropping it (issue 
#462);
+         * a no-op if a parse-time repair already named it. */
+        otp_repair_anonymous_import_token (otp, import_index);
         if (!otp_validate_import_token (otp, &validation_err)) {
             if (validation_err != NULL) {
                 g_printerr ("Skipping invalid imported token: %s\n", 
validation_err->message);
@@ -851,9 +906,13 @@
     guint added = 0, skipped = 0;
     GSList *new_hashes = NULL;
     GSList *new_data = NULL;
-    for (GSList *l = otps; l != NULL; l = l->next) {
+    gsize import_index = 0;
+    for (GSList *l = otps; l != NULL; l = l->next, import_index++) {
         otp_t *otp = l->data;
         GError *validation_err = NULL;
+        /* Keep an anonymous imported token instead of dropping it (issue 
#462);
+         * a no-op if a parse-time repair already named it. */
+        otp_repair_anonymous_import_token (otp, import_index);
         if (!otp_validate_import_token (otp, &validation_err)) {
             if (validation_err != NULL) {
                 g_printerr ("Skipping invalid imported token: %s\n", 
validation_err->message);
@@ -1497,9 +1556,34 @@
         return FALSE;
     }
 
-    gsize input_data_len = json_dumpb (json_data, NULL, 0, JSON_COMPACT);
+    // Preserve tokens set aside on load because they failed validation (issue
+    // #464): they are kept out of the live in-memory set, so merge them back 
in
+    // for serialization only, leaving the caller's json_data untouched. 
merged is
+    // a shallow copy referencing the same token objects, so freeing it never
+    // touches the underlying secrets.
+    json_t *to_dump = json_data;
+    json_t *merged = NULL;
+    if (db_data->quarantined_tokens != NULL &&
+        json_array_size (db_data->quarantined_tokens) > 0) {
+        merged = json_copy (json_data);
+        if (merged == NULL) {
+            g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
+                         "Failed to merge preserved tokens for 
serialization.");
+            explicit_bzero (derived_key, ARGON2ID_KEYLEN);
+            gcry_free (derived_key);
+            return FALSE;
+        }
+        gsize q_idx;
+        json_t *q_obj;
+        json_array_foreach (db_data->quarantined_tokens, q_idx, q_obj)
+            json_array_append (merged, q_obj);
+        to_dump = merged;
+    }
+
+    gsize input_data_len = json_dumpb (to_dump, NULL, 0, JSON_COMPACT);
     if (input_data_len == 0) {
         g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to 
serialize the in-memory database.");
+        if (merged != NULL) json_decref (merged);
         explicit_bzero (derived_key, ARGON2ID_KEYLEN);
         gcry_free (derived_key);
         return FALSE;
@@ -1508,17 +1592,20 @@
     if (in_memory_dumped_data == NULL) {
         g_set_error (err, secmem_alloc_error_gquark (), SECMEM_ALLOC_ERRCODE,
                      "Failed to allocate secure memory for serialized 
database.");
+        if (merged != NULL) json_decref (merged);
         explicit_bzero (derived_key, ARGON2ID_KEYLEN);
         gcry_free (derived_key);
         return FALSE;
     }
-    if (json_dumpb (json_data, in_memory_dumped_data, input_data_len, 
JSON_COMPACT) == (size_t) -1) {
+    if (json_dumpb (to_dump, in_memory_dumped_data, input_data_len, 
JSON_COMPACT) == (size_t) -1) {
         g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE, "Failed to 
serialize the in-memory database.");
+        if (merged != NULL) json_decref (merged);
         gcry_free (in_memory_dumped_data);
         explicit_bzero (derived_key, ARGON2ID_KEYLEN);
         gcry_free (derived_key);
         return FALSE;
     }
+    if (merged != NULL) json_decref (merged);
     guchar *enc_buffer = g_malloc0 (input_data_len);
 
     // C3 fix: in_memory_dumped_data holds the plaintext database (every 
secret).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/db-common.h 
new/OTPClient-5.1.4/src/common/db-common.h
--- old/OTPClient-5.1.1/src/common/db-common.h  2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/common/db-common.h  2026-07-10 11:41:32.000000000 
+0200
@@ -65,6 +65,13 @@
 
     json_t *committed_json_data;
 
+    /* Tokens that failed validation on load (out-of-range digits/period, 
invalid
+     * secret, unsupported algo, ...). Kept aside instead of bricking the whole
+     * database (issues #458/#462/#464): the valid tokens load, these are 
preserved
+     * verbatim and re-merged into the file on every save, and the UI surfaces 
them
+     * for the user to repair or delete. NULL until load runs. */
+    json_t *quarantined_tokens;
+
     GSList *objects_hash;
 
     GSList *data_to_add;
@@ -125,6 +132,10 @@
 
 void          database_data_free (DatabaseData *db_data);
 
+/* Number of tokens set aside on load because they failed validation (preserved
+ * in the file and surfaced to the user for repair). 0 in the normal case. */
+guint         db_get_quarantined_count (DatabaseData *db_data);
+
 void    load_db            (DatabaseData   *db_data,
                             GError        **error);
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/otp-validation.c 
new/OTPClient-5.1.4/src/common/otp-validation.c
--- old/OTPClient-5.1.1/src/common/otp-validation.c     2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/common/otp-validation.c     2026-07-10 
11:41:32.000000000 +0200
@@ -36,6 +36,17 @@
     return TRUE;
 }
 
+/* Placeholder name for a token that arrives with neither a label nor an 
issuer.
+ * Not translated: it is written into stored token data and must stay stable
+ * regardless of the locale the database is later opened under. The index keeps
+ * multiple placeholders distinct and mirrors the "Token N" position that
+ * validation reports, so the label matches the diagnostic users used to see. 
*/
+static gchar *
+otp_anonymous_placeholder (gsize index)
+{
+    return g_strdup_printf ("Unknown %" G_GSIZE_FORMAT, index);
+}
+
 gboolean
 otp_validate_token_object (json_t  *obj,
                            gsize    index,
@@ -87,7 +98,7 @@
         return FALSE;
     }
     json_int_t digits = json_integer_value (digits_obj);
-    if (digits < 6 || digits > 8) {
+    if (digits < OTP_DIGITS_MIN || digits > OTP_DIGITS_MAX) {
         g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
                      "Token %" G_GSIZE_FORMAT " has an out-of-range digits 
value.", index);
         return FALSE;
@@ -108,7 +119,7 @@
             return FALSE;
         }
         json_int_t period = json_integer_value (period_obj);
-        if (period <= 0 || period > 300) {
+        if (period < OTP_PERIOD_MIN || period > OTP_PERIOD_MAX) {
             g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
                          "Token %" G_GSIZE_FORMAT " has an out-of-range TOTP 
period.", index);
             return FALSE;
@@ -183,13 +194,13 @@
                      "Import token has an unsupported or missing algorithm.");
         return FALSE;
     }
-    if (otp->digits < 6 || otp->digits > 8) {
+    if (otp->digits < OTP_DIGITS_MIN || otp->digits > OTP_DIGITS_MAX) {
         g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
                      "Import token has an out-of-range digits value.");
         return FALSE;
     }
     if (g_ascii_strcasecmp (otp->type, "TOTP") == 0) {
-        if (otp->period == 0 || otp->period > 300) {
+        if (otp->period < OTP_PERIOD_MIN || otp->period > OTP_PERIOD_MAX) {
             g_set_error (err, generic_error_gquark (), GENERIC_ERRCODE,
                          "Import token has an out-of-range TOTP period.");
             return FALSE;
@@ -201,3 +212,84 @@
     }
     return TRUE;
 }
+
+gboolean
+otp_repair_anonymous_token_object (json_t *obj,
+                                   gsize   index)
+{
+    if (!json_is_object (obj))
+        return FALSE;
+
+    const gchar *label  = json_string_value (json_object_get (obj, "label"));
+    const gchar *issuer = json_string_value (json_object_get (obj, "issuer"));
+    gboolean has_label  = (label  != NULL && label[0]  != '\0');
+    gboolean has_issuer = (issuer != NULL && issuer[0] != '\0');
+    if (has_label || has_issuer)
+        return FALSE;
+
+    gchar *placeholder = otp_anonymous_placeholder (index);
+    json_object_set_new (obj, "label", json_string (placeholder));
+    g_free (placeholder);
+    return TRUE;
+}
+
+guint
+otp_repair_database_root (json_t *root)
+{
+    if (!json_is_array (root))
+        return 0;
+
+    guint repaired = 0;
+    gsize index;
+    json_t *obj;
+    json_array_foreach (root, index, obj) {
+        if (otp_repair_anonymous_token_object (obj, index))
+            repaired++;
+    }
+    return repaired;
+}
+
+gboolean
+otp_repair_anonymous_import_token (otp_t *otp,
+                                   gsize  index)
+{
+    if (otp == NULL)
+        return FALSE;
+
+    gboolean has_label  = (otp->account_name != NULL && otp->account_name[0] 
!= '\0');
+    gboolean has_issuer = (otp->issuer       != NULL && otp->issuer[0]       
!= '\0');
+    if (has_label || has_issuer)
+        return FALSE;
+
+    /* account_name is non-sensitive and freed with g_free across the 
importers,
+     * so g_strdup keeps the allocator contract (even while Jansson's global
+     * allocator is gcry secure memory). */
+    g_free (otp->account_name);
+    otp->account_name = otp_anonymous_placeholder (index);
+    return TRUE;
+}
+
+guint
+otp_extract_invalid_tokens (json_t *root,
+                            json_t *invalid_out)
+{
+    if (!json_is_array (root) || !json_is_array (invalid_out))
+        return 0;
+
+    guint moved = 0;
+    /* Walk backwards so removing an entry never shifts an index we still have 
to
+     * visit. Take a reference in invalid_out before dropping root's, so the
+     * token object survives the move. */
+    for (gsize i = json_array_size (root); i > 0; i--) {
+        gsize idx = i - 1;
+        json_t *obj = json_array_get (root, idx);
+        GError *verr = NULL;
+        if (otp_validate_token_object (obj, idx, &verr))
+            continue;
+        g_clear_error (&verr);
+        json_array_append (invalid_out, obj);
+        json_array_remove (root, idx);
+        moved++;
+    }
+    return moved;
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/otp-validation.h 
new/OTPClient-5.1.4/src/common/otp-validation.h
--- old/OTPClient-5.1.1/src/common/otp-validation.h     2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/common/otp-validation.h     2026-07-10 
11:41:32.000000000 +0200
@@ -2,6 +2,7 @@
 
 #include <glib.h>
 #include <jansson.h>
+#include <cotp.h>
 #include "common.h"
 
 G_BEGIN_DECLS
@@ -9,11 +10,37 @@
 /* Exclusive upper bound: valid HOTP counters satisfy counter < 2^48. */
 #define OTP_HOTP_COUNTER_MAX ((guint64) (1ULL << 48))
 
+/* Accepted OTP field ranges, kept in lockstep with what libcotp can actually
+ * generate (it hard-rejects out-of-range input, it never clamps). digits comes
+ * straight from libcotp's MIN_DIGITS/MAX_DIGITS. libcotp does not yet export
+ * period bounds, so we fall back to its check_period() range (1 to 120) until 
it
+ * does, at which point these pick the macros up automatically. Making the
+ * validator stricter than these is what locked users out in #464 (digits);
+ * making it looser than the engine (the old period ceiling of 300) admits
+ * tokens libcotp then refuses to generate. */
+#define OTP_DIGITS_MIN  MIN_DIGITS
+#define OTP_DIGITS_MAX  MAX_DIGITS
+#ifndef MIN_PERIOD
+#define MIN_PERIOD 1
+#endif
+#ifndef MAX_PERIOD
+#define MAX_PERIOD 120
+#endif
+#define OTP_PERIOD_MIN  MIN_PERIOD
+#define OTP_PERIOD_MAX  MAX_PERIOD
+
 /* Validation policy:
  * - Loaded databases are validated as a full root array before becoming live.
  * - Imported tokens are validated before entering a transaction.
  * - Direct generators (CLI/search provider) and GUI add/edit flows validate
  *   again at their local boundary so future bypasses fail closed.
+ *
+ * Repair policy (issue #462):
+ * - A token with neither a label nor an issuer is anonymous. Rejecting one
+ *   aborts the whole load, locking the user out of every other token. On the
+ *   non-interactive load and file/URI import paths we instead synthesize a
+ *   placeholder label so the token stays usable and editable. Interactive
+ *   entry (manual-add, edit) keeps rejecting: the user is right there to fix 
it.
  */
 
 gboolean otp_secret_is_valid_base32 (const gchar *secret);
@@ -28,4 +55,27 @@
 gboolean otp_validate_import_token  (const otp_t  *otp,
                                      GError      **err);
 
+/* Give an anonymous token (neither label nor issuer) a synthesized "Unknown N"
+ * label. Idempotent: a token that already has a label or issuer is left as-is.
+ * Returns TRUE when a placeholder was assigned. */
+gboolean otp_repair_anonymous_token_object (json_t      *obj,
+                                            gsize        index);
+
+/* Repair every anonymous token in a database root array in place. Returns the
+ * number of tokens that received a placeholder. */
+guint    otp_repair_database_root          (json_t      *root);
+
+/* Same as otp_repair_anonymous_token_object for an in-flight import token,
+ * writing the placeholder into otp->account_name. */
+gboolean otp_repair_anonymous_import_token (otp_t       *otp,
+                                            gsize        index);
+
+/* Move every token that fails otp_validate_token_object out of `root` and into
+ * `invalid_out` (both must be JSON arrays), preserving it for later repair
+ * instead of failing the whole load. Run it after otp_repair_database_root so 
an
+ * anonymous-but-otherwise-valid token is kept, not quarantined. Returns the
+ * number of tokens moved; `root` is left holding only tokens that validate. */
+guint    otp_extract_invalid_tokens        (json_t      *root,
+                                            json_t      *invalid_out);
+
 G_END_DECLS
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/parse-uri.c 
new/OTPClient-5.1.4/src/common/parse-uri.c
--- old/OTPClient-5.1.1/src/common/parse-uri.c  2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/common/parse-uri.c  2026-07-10 11:41:32.000000000 
+0200
@@ -241,14 +241,14 @@
     if (period != NULL) {
         gchar *endptr = NULL;
         gint64 v = g_ascii_strtoll (period, &endptr, 10);
-        if (endptr != period && *endptr == '\0' && v > 0 && v <= 300)
+        if (endptr != period && *endptr == '\0' && v >= OTP_PERIOD_MIN && v <= 
OTP_PERIOD_MAX)
             otp->period = (guint32) v;
     }
     const gchar *digits = g_hash_table_lookup (params, "digits");
     if (digits != NULL) {
         gchar *endptr = NULL;
         gint64 v = g_ascii_strtoll (digits, &endptr, 10);
-        if (endptr != digits && *endptr == '\0' && v >= 6 && v <= 8)
+        if (endptr != digits && *endptr == '\0' && v >= OTP_DIGITS_MIN && v <= 
OTP_DIGITS_MAX)
             otp->digits = (guint32) v;
     }
     const gchar *counter = g_hash_table_lookup (params, "counter");
@@ -262,6 +262,9 @@
     }
 
     GError *validation_err = NULL;
+    /* Name an anonymous token rather than rejecting it (issue #462); the
+     * commit path leaves it as-is once it carries a label. */
+    otp_repair_anonymous_import_token (otp, g_slist_length (*otps));
     if (!otp_validate_import_token (otp, &validation_err)) {
         if (validation_err != NULL)
             g_clear_error (&validation_err);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/common/twofas.c 
new/OTPClient-5.1.4/src/common/twofas.c
--- old/OTPClient-5.1.1/src/common/twofas.c     2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/common/twofas.c     2026-07-10 11:41:32.000000000 
+0200
@@ -287,8 +287,9 @@
             g_set_error (&err, generic_error_gquark (), GENERIC_ERRCODE, 
"Couldn't encrypt the reference data.");
             goto end;
         }
+        // json_string() copies the C string, it does not take ownership, so
+        // encoded_ref_data still has to be freed by us (done at `end:`).
         json_object_set_new (enc_root, "reference", json_string 
(encoded_ref_data));
-        encoded_ref_data = NULL;  // ownership transferred via json_string
 
         json_enc_data = json_dumps (enc_root, JSON_COMPACT);
         if (json_enc_data == NULL) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/dialogs/manual-add-dialog.c 
new/OTPClient-5.1.4/src/gui/dialogs/manual-add-dialog.c
--- old/OTPClient-5.1.1/src/gui/dialogs/manual-add-dialog.c     2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/gui/dialogs/manual-add-dialog.c     2026-07-10 
11:41:32.000000000 +0200
@@ -352,7 +352,7 @@
     adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), 
self->algo_combo);
 
     /* Digits */
-    GtkAdjustment *digits_adj = gtk_adjustment_new (6, 4, 10, 1, 1, 0);
+    GtkAdjustment *digits_adj = gtk_adjustment_new (6, OTP_DIGITS_MIN, 
OTP_DIGITS_MAX, 1, 1, 0);
     self->digits_spin = adw_spin_row_new (digits_adj, 1, 0);
     adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->digits_spin), 
_("Digits"));
     gtk_widget_set_tooltip_text (self->digits_spin,
@@ -360,7 +360,7 @@
     adw_preferences_group_add (ADW_PREFERENCES_GROUP (settings_group), 
self->digits_spin);
 
     /* Period (TOTP) */
-    GtkAdjustment *period_adj = gtk_adjustment_new (30, 1, 300, 1, 10, 0);
+    GtkAdjustment *period_adj = gtk_adjustment_new (30, OTP_PERIOD_MIN, 
OTP_PERIOD_MAX, 1, 10, 0);
     self->period_spin = adw_spin_row_new (period_adj, 1, 0);
     adw_preferences_row_set_title (ADW_PREFERENCES_ROW (self->period_spin), 
_("Period (seconds)"));
     gtk_widget_set_tooltip_text (self->period_spin,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/dialogs/settings-dialog.c 
new/OTPClient-5.1.4/src/gui/dialogs/settings-dialog.c
--- old/OTPClient-5.1.1/src/gui/dialogs/settings-dialog.c       2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/gui/dialogs/settings-dialog.c       2026-07-10 
11:41:32.000000000 +0200
@@ -34,6 +34,25 @@
 
 G_DEFINE_FINAL_TYPE (SettingsDialog, settings_dialog, 
ADW_TYPE_PREFERENCES_DIALOG)
 
+/* Forward declarations: the Auto-Lock and Secret Service handlers each need to
+ * switch the other off, since the two are mutually exclusive (#279). */
+static void on_auto_lock_toggled (GObject *obj, GParamSpec *pspec, 
SettingsDialog *self);
+static void on_secret_service_toggled (GObject *obj, GParamSpec *pspec, 
SettingsDialog *self);
+
+static void
+sync_lock_exclusivity (SettingsDialog *self)
+{
+    gboolean auto_lock = adw_switch_row_get_active (ADW_SWITCH_ROW 
(self->auto_lock_switch));
+    gboolean secret_service = adw_switch_row_get_active (ADW_SWITCH_ROW 
(self->secret_service_switch));
+
+    /* A lock is meaningless when the password lives in the keyring (it is
+     * bypassed by a restart), so whichever feature is on greys out and 
disables
+     * the other. */
+    gtk_widget_set_sensitive (self->auto_lock_switch, !secret_service);
+    gtk_widget_set_sensitive (self->secret_service_switch, !auto_lock);
+    gtk_widget_set_sensitive (self->inactivity_combo, auto_lock && 
!secret_service);
+}
+
 static void
 on_show_next_otp_toggled (GObject        *obj,
                           GParamSpec     *pspec,
@@ -87,7 +106,20 @@
     (void) pspec;
     gboolean active = adw_switch_row_get_active (ADW_SWITCH_ROW (obj));
     otpclient_application_set_auto_lock (self->app, active);
-    gtk_widget_set_sensitive (self->inactivity_combo, active);
+
+    /* Enabling Auto-Lock turns Secret Service off (#279). */
+    if (active &&
+        adw_switch_row_get_active (ADW_SWITCH_ROW 
(self->secret_service_switch)))
+    {
+        g_signal_handlers_block_by_func (self->secret_service_switch,
+                                         on_secret_service_toggled, self);
+        adw_switch_row_set_active (ADW_SWITCH_ROW 
(self->secret_service_switch), FALSE);
+        g_signal_handlers_unblock_by_func (self->secret_service_switch,
+                                           on_secret_service_toggled, self);
+        otpclient_application_set_use_secret_service (self->app, FALSE);
+    }
+
+    sync_lock_exclusivity (self);
 }
 
 static void
@@ -130,6 +162,7 @@
 
     if (!active) {
         otpclient_application_set_use_secret_service (self->app, FALSE);
+        sync_lock_exclusivity (self);
         return;
     }
 
@@ -140,6 +173,19 @@
     GError *err = NULL;
     if (otpclient_secret_service_probe (&err)) {
         otpclient_application_set_use_secret_service (self->app, TRUE);
+
+        /* Enabling Secret Service turns Auto-Lock off (#279). */
+        if (adw_switch_row_get_active (ADW_SWITCH_ROW 
(self->auto_lock_switch)))
+        {
+            g_signal_handlers_block_by_func (self->auto_lock_switch,
+                                             on_auto_lock_toggled, self);
+            adw_switch_row_set_active (ADW_SWITCH_ROW 
(self->auto_lock_switch), FALSE);
+            g_signal_handlers_unblock_by_func (self->auto_lock_switch,
+                                               on_auto_lock_toggled, self);
+            otpclient_application_set_auto_lock (self->app, FALSE);
+        }
+
+        sync_lock_exclusivity (self);
         return;
     }
 
@@ -513,8 +559,6 @@
             break;
         }
     }
-    gtk_widget_set_sensitive (self->inactivity_combo,
-                              otpclient_application_get_auto_lock (app));
     g_signal_connect (self->inactivity_combo, "notify::selected",
                       G_CALLBACK (on_inactivity_changed), self);
     adw_preferences_group_add (security_group, self->inactivity_combo);
@@ -558,6 +602,22 @@
                       G_CALLBACK (on_secret_service_toggled), self);
     adw_preferences_group_add (security_group, self->secret_service_switch);
 
+    /* #279: Auto-Lock and Secret Service are mutually exclusive. Reconcile a
+     * legacy profile that has both enabled (Secret Service wins, since the 
lock
+     * is bypassable via restart when the password is in the keyring), then set
+     * the initial greyed-out state. */
+    if (otpclient_application_get_auto_lock (app) &&
+        otpclient_application_get_use_secret_service (app))
+    {
+        g_signal_handlers_block_by_func (self->auto_lock_switch,
+                                         on_auto_lock_toggled, self);
+        adw_switch_row_set_active (ADW_SWITCH_ROW (self->auto_lock_switch), 
FALSE);
+        g_signal_handlers_unblock_by_func (self->auto_lock_switch,
+                                           on_auto_lock_toggled, self);
+        otpclient_application_set_auto_lock (self->app, FALSE);
+    }
+    sync_lock_exclusivity (self);
+
     /* Integration group */
     AdwPreferencesGroup *integration_group = ADW_PREFERENCES_GROUP 
(adw_preferences_group_new ());
     adw_preferences_group_set_title (integration_group, _("Integration"));
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/google-migration.c 
new/OTPClient-5.1.4/src/gui/google-migration.c
--- old/OTPClient-5.1.1/src/gui/google-migration.c      2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/gui/google-migration.c      2026-07-10 
11:41:32.000000000 +0200
@@ -165,6 +165,8 @@
         sensitive_free (base32);
 
         GError *validation_error = NULL;
+        /* Name an anonymous token rather than dropping it (issue #462). */
+        otp_repair_anonymous_import_token (otp, g_slist_length (result));
         if (otp->secret == NULL ||
             !otp_validate_import_token (otp, &validation_error)) {
             g_clear_error (&validation_error);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/lock-app.c 
new/OTPClient-5.1.4/src/gui/lock-app.c
--- old/OTPClient-5.1.1/src/gui/lock-app.c      2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/gui/lock-app.c      2026-07-10 11:41:32.000000000 
+0200
@@ -177,7 +177,11 @@
     gboolean active = FALSE;
 
     g_variant_get (parameters, "(b)", &active);
-    if (active)
+
+    /* Only auto-lock on session lock when Auto-Lock is enabled, matching the
+     * inactivity timer. Without this guard the app locks on every screen lock
+     * even with Auto-Lock off (#460, a re-report of #279). */
+    if (active && otpclient_application_get_auto_lock (app))
         lock_app_lock (app);
 }
 
@@ -196,10 +200,13 @@
     (void) interface_name;
     (void) signal_name;
 
+    OTPClientApplication *app = OTPCLIENT_APPLICATION (user_data);
     gboolean preparing = FALSE;
     g_variant_get (parameters, "(b)", &preparing);
-    if (preparing)
-        lock_app_lock (OTPCLIENT_APPLICATION (user_data));
+
+    /* Respect Auto-Lock here too (see on_screensaver_signal / #460). */
+    if (preparing && otpclient_application_get_auto_lock (app))
+        lock_app_lock (app);
 }
 
 static gboolean
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/otp-entry.c 
new/OTPClient-5.1.4/src/gui/otp-entry.c
--- old/OTPClient-5.1.1/src/gui/otp-entry.c     2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/src/gui/otp-entry.c     2026-07-10 11:41:32.000000000 
+0200
@@ -4,6 +4,7 @@
 #include <limits.h>
 #include "common.h"
 #include "otp-entry.h"
+#include "otp-validation.h"
 
 struct _OTPEntry
 {
@@ -256,7 +257,7 @@
         g_param_spec_string ("otp-type", NULL, NULL, "TOTP",
                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS);
     properties[PROP_PERIOD] =
-        g_param_spec_uint ("period", NULL, NULL, 1, 300, 30,
+        g_param_spec_uint ("period", NULL, NULL, OTP_PERIOD_MIN, 
OTP_PERIOD_MAX, 30,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS);
     properties[PROP_COUNTER] =
         g_param_spec_uint64 ("counter", NULL, NULL, 0, G_MAXUINT64, 0,
@@ -265,7 +266,7 @@
         g_param_spec_string ("algorithm", NULL, NULL, "SHA1",
                              G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS);
     properties[PROP_DIGITS] =
-        g_param_spec_uint ("digits", NULL, NULL, 4, 10, 6,
+        g_param_spec_uint ("digits", NULL, NULL, OTP_DIGITS_MIN, 
OTP_DIGITS_MAX, 6,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | 
G_PARAM_STATIC_STRINGS);
     properties[PROP_SECRET] =
         g_param_spec_string ("secret", NULL, NULL, NULL,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/otpclient-application.c 
new/OTPClient-5.1.4/src/gui/otpclient-application.c
--- old/OTPClient-5.1.1/src/gui/otpclient-application.c 2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/gui/otpclient-application.c 2026-07-10 
11:41:32.000000000 +0200
@@ -726,6 +726,20 @@
         otpclient_window_hide_loading (self->window);
         otpclient_window_start_otp_timer (self->window);
         otpclient_window_sync_active_flag (self->window);
+
+        /* Issue #464: tokens that failed validation were set aside so the rest
+         * of the database could open. Tell the user they are preserved; the
+         * dedicated repair dialog arrives in 5.2.0. */
+        guint quarantined = db_get_quarantined_count (self->db_data);
+        if (quarantined > 0)
+        {
+            g_autofree gchar *msg = g_strdup_printf (
+                ngettext ("%u token could not be loaded and was kept for 
repair",
+                          "%u tokens could not be loaded and were kept for 
repair",
+                          quarantined),
+                quarantined);
+            otpclient_window_show_error_toast (self->window, msg);
+        }
     }
     if (self->app_locked)
         lock_app_unlock (self);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/src/gui/otpclient-window.c 
new/OTPClient-5.1.4/src/gui/otpclient-window.c
--- old/OTPClient-5.1.1/src/gui/otpclient-window.c      2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/gui/otpclient-window.c      2026-07-10 
11:41:32.000000000 +0200
@@ -3748,6 +3748,7 @@
 }
 
 static void on_popover_closed (GtkPopover *popover, gpointer user_data);
+static void popup_menu_at (GtkWidget *popover, GtkWidget *over, double x, 
double y);
 
 static void
 on_token_right_click (GtkGestureClick *gesture,
@@ -3870,11 +3871,7 @@
     g_object_unref (group_submenu);
 
     GtkWidget *popover = gtk_popover_menu_new_from_model (G_MENU_MODEL (menu));
-    gtk_widget_set_parent (popover, self->otp_list);
-    GdkRectangle rect = { (int)x, (int)y, 1, 1 };
-    gtk_popover_set_pointing_to (GTK_POPOVER (popover), &rect);
-    g_signal_connect (popover, "closed", G_CALLBACK (on_popover_closed), NULL);
-    gtk_popover_popup (GTK_POPOVER (popover));
+    popup_menu_at (popover, self->otp_list, x, y);
 
     g_object_unref (menu);
     g_object_unref (builder);
@@ -4338,6 +4335,37 @@
     g_idle_add (popover_unparent_idle, popover);
 }
 
+/* Present a context popover menu pointing at (x, y) in `over`'s coordinate
+ * space. The popover is parented to `over`'s nearest non-scrolled GtkBox
+ * ancestor, not to the list itself: a popover inside a GtkScrolledWindow is
+ * clipped to the scrolled viewport, which made GtkPopoverMenu scroll even for 
a
+ * handful of items (issue #465). Coordinates are translated into the anchor's
+ * space so the menu still points at the clicked row. */
+static void
+popup_menu_at (GtkWidget *popover,
+               GtkWidget *over,
+               double     x,
+               double     y)
+{
+    GtkWidget *anchor = gtk_widget_get_ancestor (over, GTK_TYPE_BOX);
+    if (anchor == NULL)
+        anchor = over;
+
+    graphene_point_t pt;
+    if (!gtk_widget_compute_point (over, anchor,
+                                   &GRAPHENE_POINT_INIT ((float) x, (float) 
y), &pt))
+    {
+        pt.x = (float) x;
+        pt.y = (float) y;
+    }
+
+    gtk_widget_set_parent (popover, anchor);
+    GdkRectangle rect = { (int) pt.x, (int) pt.y, 1, 1 };
+    gtk_popover_set_pointing_to (GTK_POPOVER (popover), &rect);
+    g_signal_connect (popover, "closed", G_CALLBACK (on_popover_closed), NULL);
+    gtk_popover_popup (GTK_POPOVER (popover));
+}
+
 static void
 on_db_right_click (GtkGestureClick *gesture,
                    gint             n_press,
@@ -4363,11 +4391,7 @@
         gtk_builder_get_object (builder, "db_context_menu"));
 
     GtkWidget *popover = gtk_popover_menu_new_from_model (menu_model);
-    gtk_widget_set_parent (popover, self->database_list);
-    GdkRectangle rect = { (int)x, (int)y, 1, 1 };
-    gtk_popover_set_pointing_to (GTK_POPOVER (popover), &rect);
-    g_signal_connect (popover, "closed", G_CALLBACK (on_popover_closed), NULL);
-    gtk_popover_popup (GTK_POPOVER (popover));
+    popup_menu_at (popover, self->database_list, x, y);
 
     g_object_unref (builder);
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/OTPClient-5.1.1/src/search-provider/search-provider.c 
new/OTPClient-5.1.4/src/search-provider/search-provider.c
--- old/OTPClient-5.1.1/src/search-provider/search-provider.c   2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/src/search-provider/search-provider.c   2026-07-10 
11:41:32.000000000 +0200
@@ -531,6 +531,12 @@
         return;
     }
 
+    /* Issue #464: broken tokens are set aside so search still works with the
+     * rest; note it in the journal without spamming (this reloads on a TTL). 
*/
+    guint quarantined = db_get_quarantined_count (db_data);
+    if (quarantined > 0)
+        g_info ("%u token(s) in '%s' could not be loaded and were skipped.", 
quarantined, db_path);
+
     kdf_cache_capture_from_db_data (db_data, db_path);
 
     gsize index;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/CMakeLists.txt 
new/OTPClient-5.1.4/tests/CMakeLists.txt
--- old/OTPClient-5.1.1/tests/CMakeLists.txt    2026-06-30 09:26:48.000000000 
+0200
+++ new/OTPClient-5.1.4/tests/CMakeLists.txt    2026-07-10 11:41:32.000000000 
+0200
@@ -165,6 +165,21 @@
 target_link_libraries(test_common_utils ${COMMON_LIBS})
 add_test(NAME common_utils COMMAND test_common_utils)
 
+add_executable(test_field_bounds_roundtrip
+        test_field_bounds_roundtrip.c
+        ${PROJECT_SOURCE_DIR}/src/common/common.c
+        ${PROJECT_SOURCE_DIR}/src/common/db-common.c
+        ${PROJECT_SOURCE_DIR}/src/common/file-size.c
+        ${PROJECT_SOURCE_DIR}/src/common/gquarks.c
+        ${PROJECT_SOURCE_DIR}/src/common/otp-validation.c
+)
+otpclient_apply_target_settings(test_field_bounds_roundtrip)
+target_include_directories(test_field_bounds_roundtrip PRIVATE
+        ${PROJECT_SOURCE_DIR}/src/common
+)
+target_link_libraries(test_field_bounds_roundtrip ${COMMON_LIBS})
+add_test(NAME field_bounds_roundtrip COMMAND test_field_bounds_roundtrip)
+
 if(BUILD_GUI)
     add_executable(test_otp_entry
             test_otp_entry.c
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/README.md 
new/OTPClient-5.1.4/tests/README.md
--- old/OTPClient-5.1.1/tests/README.md 2026-06-30 09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/tests/README.md 2026-07-10 11:41:32.000000000 +0200
@@ -74,3 +74,12 @@
 token records, the hex string helpers, `secure_strdup`, and the algorithm
 name -> integer mapping. The validator tests pin every required-field and
 range check so a refactor of the validation policy can't silently relax it.
+
+**`test_field_bounds_roundtrip`** is the regression guard for the database-load
+lockout class (#458/#462/#464). It pins the parity invariant that every digit
+count and TOTP period libcotp can actually generate (4 to 10, 1 to 120) passes
+both the load-time and import validators, so nothing a producer emits is later
+rejected on open; that the validators still reject values the engine cannot 
use;
+and that a database file which does contain such a token still opens - the 
valid
+tokens load while the broken ones are set aside and preserved across a
+save/reload instead of bricking the whole database.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/test_db_roundtrip.c 
new/OTPClient-5.1.4/tests/test_db_roundtrip.c
--- old/OTPClient-5.1.1/tests/test_db_roundtrip.c       2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/tests/test_db_roundtrip.c       2026-07-10 
11:41:32.000000000 +0200
@@ -209,6 +209,68 @@
     cleanup_tmp_db (writer, dir, path);
 }
 
+static otp_t *
+anonymous_totp (void)
+{
+    otp_t *otp = g_new0 (otp_t, 1);
+    otp->type = g_strdup ("TOTP");
+    otp->algo = g_strdup ("SHA1");
+    otp->digits = 6;
+    otp->period = 30;
+    otp->account_name = NULL;   /* neither a label ... */
+    otp->issuer = NULL;         /* ... nor an issuer */
+    otp->secret = secure_strdup ("JBSWY3DPEHPK3PXP");
+    return otp;
+}
+
+static void
+free_test_otp (otp_t *otp)
+{
+    g_free (otp->type);
+    g_free (otp->algo);
+    g_free (otp->account_name);
+    g_free (otp->issuer);
+    gcry_free (otp->secret);
+    g_free (otp);
+}
+
+static void
+test_import_anonymous_token_roundtrips (void)
+{
+    /* An imported token with neither a label nor an issuer used to be silently
+     * dropped. It now survives with a synthesized label and roundtrips through
+     * the real encrypt/decrypt path (issue #462). */
+    gchar *path = NULL;
+    gchar *dir = make_tmp_db_dir (&path);
+
+    DatabaseData *writer = make_db_data_with_path (path, "test-password");
+    GError *err = NULL;
+    update_db (writer, &err);           /* seed with alice + bob */
+    g_assert_no_error (err);
+
+    GSList *otps = g_slist_append (NULL, anonymous_totp ());
+    guint added = 0, skipped = 0;
+    add_otps_to_db_ex (otps, writer, &added, &skipped);
+    g_assert_cmpuint (added, ==, 1);
+    g_assert_cmpuint (skipped, ==, 0);
+
+    update_db (writer, &err);           /* persist the staged import */
+    g_assert_no_error (err);
+
+    DatabaseData *reader = database_data_new (path, DEFAULT_MEMLOCK_VALUE);
+    reader->key = secure_strdup ("test-password");
+    load_db (reader, &err);
+    g_assert_no_error (err);
+    g_assert_cmpuint (json_array_size (reader->in_memory_json_data), ==, 3);
+
+    json_t *imported = json_array_get (reader->in_memory_json_data, 2);
+    g_assert_cmpstr (json_string_value (json_object_get (imported, "label")), 
==, "Unknown 0");
+
+    g_slist_free_full (otps, (GDestroyNotify) free_test_otp);
+    database_data_free (reader);
+    cleanup_tmp_db (writer, dir, path);
+}
+
 static void
 test_purge_then_reload (void)
 {
@@ -246,6 +308,7 @@
     g_test_add_func ("/db-roundtrip/corrupted-ciphertext", 
test_corrupted_ciphertext_rejected);
     g_test_add_func ("/db-roundtrip/password-change",      
test_password_change_then_reload);
     g_test_add_func ("/db-roundtrip/kdf-param-update",     
test_kdf_param_update_then_reload);
+    g_test_add_func ("/db-roundtrip/import-anonymous",     
test_import_anonymous_token_roundtrips);
     g_test_add_func ("/db-roundtrip/purge-then-reload",    
test_purge_then_reload);
 
     return g_test_run ();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/test_field_bounds_roundtrip.c 
new/OTPClient-5.1.4/tests/test_field_bounds_roundtrip.c
--- old/OTPClient-5.1.1/tests/test_field_bounds_roundtrip.c     1970-01-01 
01:00:00.000000000 +0100
+++ new/OTPClient-5.1.4/tests/test_field_bounds_roundtrip.c     2026-07-10 
11:41:32.000000000 +0200
@@ -0,0 +1,200 @@
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <jansson.h>
+#include "common.h"
+#include "db-common.h"
+#include "otp-validation.h"
+#include "gquarks.h"
+
+/* Regression guard for the database-load lockout class (#458/#462/#464): the
+ * loader validator drifted stricter than what libcotp and the app's own UI can
+ * produce, so a single legitimate token bricked the whole database. Two
+ * invariants pin it shut:
+ *   1. Every value in libcotp's real range passes both validators (parity),
+ *      so nothing a producer can emit is later rejected on load.
+ *   2. A file that still contains tokens the engine cannot use opens anyway:
+ *      the valid tokens load, the broken ones are quarantined and preserved
+ *      across save/reload, and the open never fails. */
+
+static json_t *
+totp_obj (const gchar *label, guint digits, const gchar *algo, guint period)
+{
+    return build_json_obj ("TOTP", label, "Example", "JBSWY3DPEHPK3PXP",
+                           digits, algo, period, 0, NULL);
+}
+
+/* Wrap a single token in a root array and run the load-time validator. 
Consumes
+ * the token reference. */
+static gboolean
+root_accepts (json_t *token)
+{
+    json_t *root = json_array ();
+    json_array_append_new (root, token);
+    GError *err = NULL;
+    gboolean ok = otp_validate_database_root (root, &err);
+    g_clear_error (&err);
+    json_decref (root);
+    return ok;
+}
+
+/* Parity: every digit count and period libcotp accepts (4-10, 1-120) must pass
+ * the loader validator, across all algorithms. Under the old 6-8 digits bound
+ * this tripped on 4/5/9/10 - exactly the #464 lockout. */
+static void
+test_engine_valid_values_accepted (void)
+{
+    const char *algos[] = { "SHA1", "SHA256", "SHA512" };
+    for (guint d = OTP_DIGITS_MIN; d <= OTP_DIGITS_MAX; d++)
+        for (guint a = 0; a < G_N_ELEMENTS (algos); a++)
+            g_assert_true (root_accepts (totp_obj ("acc", d, algos[a], 30)));
+
+    const guint periods[] = { OTP_PERIOD_MIN, 15, 30, 60, 90, OTP_PERIOD_MAX };
+    for (guint p = 0; p < G_N_ELEMENTS (periods); p++)
+        g_assert_true (root_accepts (totp_obj ("acc", 6, "SHA1", periods[p])));
+}
+
+/* The import-path validator (save / manual-add / import) must agree with the
+ * loader on the same range, so a token accepted on one path is never rejected
+ * on the other. */
+static void
+test_import_validator_matches_range (void)
+{
+    for (guint d = OTP_DIGITS_MIN; d <= OTP_DIGITS_MAX; d++) {
+        otp_t otp = { .type = "TOTP", .account_name = "acc", .issuer = 
"Example",
+                      .secret = "JBSWY3DPEHPK3PXP", .digits = d, .algo = 
"SHA1" };
+        otp.period = 30;
+        GError *err = NULL;
+        g_assert_true (otp_validate_import_token (&otp, &err));
+        g_clear_error (&err);
+    }
+    const guint periods[] = { OTP_PERIOD_MIN, 30, OTP_PERIOD_MAX };
+    for (guint p = 0; p < G_N_ELEMENTS (periods); p++) {
+        otp_t otp = { .type = "TOTP", .account_name = "acc", .issuer = 
"Example",
+                      .secret = "JBSWY3DPEHPK3PXP", .digits = 6, .algo = 
"SHA1" };
+        otp.period = periods[p];
+        GError *err = NULL;
+        g_assert_true (otp_validate_import_token (&otp, &err));
+        g_clear_error (&err);
+    }
+}
+
+/* The validator must stay strict for values libcotp cannot generate, so the
+ * save/import paths still refuse to introduce them. */
+static void
+test_out_of_range_rejected (void)
+{
+    g_assert_false (root_accepts (totp_obj ("acc", OTP_DIGITS_MIN - 1, "SHA1", 
30)));
+    g_assert_false (root_accepts (totp_obj ("acc", OTP_DIGITS_MAX + 1, "SHA1", 
30)));
+    g_assert_false (root_accepts (totp_obj ("acc", 6, "SHA1", OTP_PERIOD_MAX + 
1)));
+    g_assert_false (root_accepts (totp_obj ("acc", 6, "SHA1", 0)));
+    g_assert_false (root_accepts (totp_obj ("acc", 6, "MD5", 30)));
+}
+
+/* ---- never-brick round-trip (real encrypt/decrypt) ---- */
+
+static gchar *
+make_tmp_db_dir (gchar **path_out)
+{
+    GError *err = NULL;
+    gchar *dir = g_dir_make_tmp ("otpclient-fb-XXXXXX", &err);
+    g_assert_no_error (err);
+    g_assert_nonnull (dir);
+    *path_out = g_build_filename (dir, "test.enc", NULL);
+    return dir;
+}
+
+static void
+cleanup_tmp_db (DatabaseData *db_data, gchar *dir, gchar *path)
+{
+    if (db_data != NULL)
+        database_data_free (db_data);
+    g_unlink (path);
+    g_autofree gchar *lock_path = g_strconcat (path, ".lock", NULL);
+    g_unlink (lock_path);
+    g_autofree gchar *bak_path = g_strconcat (path, ".bak", NULL);
+    g_unlink (bak_path);
+    g_rmdir (dir);
+    g_free (path);
+    g_free (dir);
+}
+
+static DatabaseData *
+open_writer (const gchar *path, const gchar *pw)
+{
+    DatabaseData *db = database_data_new (path, DEFAULT_MEMLOCK_VALUE);
+    db->key = secure_strdup (pw);
+    db->argon2id_iter = ARGON2ID_MIN_ITER;
+    db->argon2id_memcost = ARGON2ID_MIN_MC;
+    db->argon2id_parallelism = ARGON2ID_MIN_PARAL;
+    db->current_db_version = DB_VERSION;
+    db->in_memory_json_data = json_array ();
+    json_array_append_new (db->in_memory_json_data, totp_obj ("alice", 6, 
"SHA1", 30));
+    json_array_append_new (db->in_memory_json_data, totp_obj ("bob", 8, 
"SHA512", 60));
+    return db;
+}
+
+static DatabaseData *
+open_reader (const gchar *path, const gchar *pw, GError **err)
+{
+    DatabaseData *db = database_data_new (path, DEFAULT_MEMLOCK_VALUE);
+    db->key = secure_strdup (pw);
+    load_db (db, err);
+    return db;
+}
+
+static void
+test_broken_tokens_quarantined_not_bricking (void)
+{
+    gchar *path = NULL;
+    gchar *dir = make_tmp_db_dir (&path);
+
+    /* Seed 2 valid tokens and stage 3 the engine cannot use as "quarantined",
+     * so encrypt_db writes them into the file exactly as a legacy or corrupt
+     * database would carry them: an out-of-range digit count, an out-of-range
+     * period, and an invalid Base32 secret. */
+    DatabaseData *writer = open_writer (path, "pw");
+    writer->quarantined_tokens = json_array ();
+    json_array_append_new (writer->quarantined_tokens, totp_obj ("bad-digits", 
12, "SHA1", 30));
+    json_array_append_new (writer->quarantined_tokens, totp_obj ("bad-period", 
6, "SHA1", 300));
+    json_array_append_new (writer->quarantined_tokens,
+        build_json_obj ("TOTP", "bad-secret", "Example", "0000", 6, "SHA1", 
30, 0, NULL));
+
+    GError *err = NULL;
+    update_db (writer, &err);
+    g_assert_no_error (err);
+
+    /* Fresh open must succeed (not brick), load only the valid tokens, and set
+     * the broken ones aside. */
+    DatabaseData *reader = open_reader (path, "pw", &err);
+    g_assert_no_error (err);
+    g_assert_nonnull (reader->in_memory_json_data);
+    g_assert_cmpint (json_array_size (reader->in_memory_json_data), ==, 2);
+    g_assert_cmpuint (db_get_quarantined_count (reader), ==, 3);
+
+    /* Saving and reloading must preserve the broken tokens, never drop them. 
*/
+    update_db (reader, &err);
+    g_assert_no_error (err);
+    DatabaseData *reader2 = open_reader (path, "pw", &err);
+    g_assert_no_error (err);
+    g_assert_cmpint (json_array_size (reader2->in_memory_json_data), ==, 2);
+    g_assert_cmpuint (db_get_quarantined_count (reader2), ==, 3);
+
+    database_data_free (reader2);
+    database_data_free (reader);
+    cleanup_tmp_db (writer, dir, path);
+}
+
+int
+main (int argc, char **argv)
+{
+    g_test_init (&argc, &argv, NULL);
+    gchar *init_err = init_libs (DEFAULT_MEMLOCK_VALUE);
+    g_assert_null (init_err);
+
+    g_test_add_func ("/field-bounds/engine-valid-accepted", 
test_engine_valid_values_accepted);
+    g_test_add_func ("/field-bounds/import-matches-range",  
test_import_validator_matches_range);
+    g_test_add_func ("/field-bounds/out-of-range-rejected", 
test_out_of_range_rejected);
+    g_test_add_func ("/field-bounds/broken-quarantined",    
test_broken_tokens_quarantined_not_bricking);
+
+    return g_test_run ();
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/test_otp_validation.c 
new/OTPClient-5.1.4/tests/test_otp_validation.c
--- old/OTPClient-5.1.1/tests/test_otp_validation.c     2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/tests/test_otp_validation.c     2026-07-10 
11:41:32.000000000 +0200
@@ -80,6 +80,97 @@
     json_decref (root);
 }
 
+static void
+test_anonymous_repaired_then_valid (void)
+{
+    /* A token with neither a label nor an issuer is rejected outright, but the
+     * load path repairs it first so the whole database still opens (issue 
#462). */
+    json_t *root = json_array ();
+    json_t *obj = valid_totp ();
+    json_object_set_new (obj, "label", json_string (""));
+    json_object_set_new (obj, "issuer", json_string (""));
+    json_array_append_new (root, obj);
+
+    GError *err = NULL;
+    g_assert_false (otp_validate_database_root (root, &err));
+    g_clear_error (&err);
+
+    g_assert_cmpuint (otp_repair_database_root (root), ==, 1);
+    g_assert_true (otp_validate_database_root (root, &err));
+    g_assert_no_error (err);
+
+    json_t *repaired = json_array_get (root, 0);
+    g_assert_cmpstr (json_string_value (json_object_get (repaired, "label")), 
==, "Unknown 0");
+
+    /* Idempotent: a second pass finds nothing anonymous and changes nothing. 
*/
+    g_assert_cmpuint (otp_repair_database_root (root), ==, 0);
+    g_assert_cmpstr (json_string_value (json_object_get (repaired, "label")), 
==, "Unknown 0");
+
+    json_decref (root);
+}
+
+static void
+test_repair_leaves_named_tokens (void)
+{
+    /* Labeled and issuer-only tokens must be left untouched. */
+    json_t *root = json_array ();
+    json_array_append_new (root, valid_totp ());
+    json_t *issuer_only = valid_totp ();
+    json_object_set_new (issuer_only, "label", json_string (""));
+    json_array_append_new (root, issuer_only);
+
+    g_assert_cmpuint (otp_repair_database_root (root), ==, 0);
+    g_assert_cmpstr (json_string_value (json_object_get (json_array_get (root, 
0), "label")), ==, "alice");
+    g_assert_cmpstr (json_string_value (json_object_get (json_array_get (root, 
1), "label")), ==, "");
+
+    json_decref (root);
+}
+
+static void
+test_repair_indexes_by_position (void)
+{
+    /* The placeholder carries the token's array index, matching the "Token N"
+     * position that validation used to report. */
+    json_t *root = json_array ();
+    json_array_append_new (root, valid_totp ());          /* index 0: named */
+    json_t *anon = valid_totp ();
+    json_object_set_new (anon, "label", json_string (""));
+    json_object_set_new (anon, "issuer", json_string (""));
+    json_array_append_new (root, anon);                   /* index 1: 
anonymous */
+
+    g_assert_cmpuint (otp_repair_database_root (root), ==, 1);
+    g_assert_cmpstr (json_string_value (json_object_get (json_array_get (root, 
1), "label")), ==, "Unknown 1");
+
+    json_decref (root);
+}
+
+static void
+test_repair_import_token (void)
+{
+    otp_t otp = {0};
+    otp.type = g_strdup ("TOTP");
+    otp.algo = g_strdup ("SHA1");
+    otp.digits = 6;
+    otp.period = 30;
+    otp.secret = g_strdup ("JBSWY3DPEHPK3PXP");
+
+    g_assert_true (otp_repair_anonymous_import_token (&otp, 3));
+    g_assert_cmpstr (otp.account_name, ==, "Unknown 3");
+
+    GError *err = NULL;
+    g_assert_true (otp_validate_import_token (&otp, &err));
+    g_assert_no_error (err);
+
+    /* Idempotent: an already-named token is not renamed. */
+    g_assert_false (otp_repair_anonymous_import_token (&otp, 9));
+    g_assert_cmpstr (otp.account_name, ==, "Unknown 3");
+
+    g_free (otp.type);
+    g_free (otp.algo);
+    g_free (otp.account_name);
+    g_free (otp.secret);
+}
+
 int
 main (int argc, char **argv)
 {
@@ -92,6 +183,10 @@
     g_test_add_func ("/validation/root-array", test_root_must_be_array);
     g_test_add_func ("/validation/zero-period", test_zero_period_rejected);
     g_test_add_func ("/validation/hotp-overflow", 
test_hotp_counter_overflow_rejected);
+    g_test_add_func ("/validation/anonymous-repaired", 
test_anonymous_repaired_then_valid);
+    g_test_add_func ("/validation/repair-leaves-named", 
test_repair_leaves_named_tokens);
+    g_test_add_func ("/validation/repair-index", 
test_repair_indexes_by_position);
+    g_test_add_func ("/validation/repair-import-token", 
test_repair_import_token);
 
     return g_test_run ();
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/OTPClient-5.1.1/tests/test_parse_uri_extra.c 
new/OTPClient-5.1.4/tests/test_parse_uri_extra.c
--- old/OTPClient-5.1.1/tests/test_parse_uri_extra.c    2026-06-30 
09:26:48.000000000 +0200
+++ new/OTPClient-5.1.4/tests/test_parse_uri_extra.c    2026-07-10 
11:41:32.000000000 +0200
@@ -80,7 +80,7 @@
 static void
 test_period_out_of_bounds (void)
 {
-    /* period=0 and period=999 fall outside (0,300]; parse_uri keeps the
+    /* period=0 and period=999 fall outside [1,120]; parse_uri keeps the
      * default of 30 in those cases. The resulting otp_t is still valid. */
     otp_t *zero = parse_single (
         
"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example&period=0");
@@ -112,6 +112,41 @@
 }
 
 static void
+test_digits_widened_range (void)
+{
+    /* digits 4-10 is libcotp's real range (issue #464). Values the old 6-8
+     * clamp used to reset to 6 - 4, 5, 9, 10 - must now be preserved. */
+    const guint kept[] = { 4, 5, 9, 10 };
+    for (guint i = 0; i < G_N_ELEMENTS (kept); i++) {
+        g_autofree gchar *uri = g_strdup_printf (
+            
"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=%u",
+            kept[i]);
+        otp_t *otp = parse_single (uri);
+        g_assert_nonnull (otp);
+        g_assert_cmpuint (otp->digits, ==, kept[i]);
+        free_otp (otp);
+    }
+}
+
+static void
+test_period_upper_bound (void)
+{
+    /* 120 is the top of libcotp's range and must be kept; 150 is above it and
+     * falls back to the default 30 (issue #464). */
+    otp_t *at_max = parse_single (
+        
"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example&period=120");
+    g_assert_nonnull (at_max);
+    g_assert_cmpuint (at_max->period, ==, 120);
+    free_otp (at_max);
+
+    otp_t *over = parse_single (
+        
"otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example&period=150");
+    g_assert_nonnull (over);
+    g_assert_cmpuint (over->period, ==, 30);
+    free_otp (over);
+}
+
+static void
 test_counter_at_max (void)
 {
     g_autofree gchar *at_max = g_strdup_printf (
@@ -214,6 +249,8 @@
     g_test_add_func ("/parse-uri-extra/unknown-algorithm",     
test_unknown_algorithm_falls_back_to_sha1);
     g_test_add_func ("/parse-uri-extra/period-out-of-bounds",  
test_period_out_of_bounds);
     g_test_add_func ("/parse-uri-extra/digits-out-of-bounds",  
test_digits_out_of_bounds);
+    g_test_add_func ("/parse-uri-extra/digits-widened-range",  
test_digits_widened_range);
+    g_test_add_func ("/parse-uri-extra/period-upper-bound",    
test_period_upper_bound);
     g_test_add_func ("/parse-uri-extra/counter-at-max",        
test_counter_at_max);
     g_test_add_func ("/parse-uri-extra/malformed-scheme",      
test_malformed_scheme_ignored);
     g_test_add_func ("/parse-uri-extra/invalid-secret",        
test_invalid_secret_rejected);

Reply via email to