aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-25 12:46:42 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-25 12:46:42 +0100
commite0384274bbeab999279bdc35bdddbd8a3a451aeb (patch)
tree206abf2390f6bd30d1ddbcc02717f1009b4cc659
parent6b17954f654d83c754cbdaae37f98689b9a4fcb7 (diff)
docs: record the settings spike and the Forerunner 70 benchmark
Spike (2026-10-21, Android): 8000 characters per field and 64,000 in total arrive; URIs with & save; the mixed special-character string is refused, so the test strings now split it into one group each plus a fake Aegis export. Benchmark: SHA-1 TOTP 18 ms, SHA-512 732 ms, PBKDF2 2,000 iterations per second. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--README.md5
-rw-r--r--docs/PERFORMANCE.md17
-rw-r--r--docs/SETTINGS_SPIKE.md42
-rw-r--r--spike/settings/test-strings/aegis-json.txt5
-rw-r--r--spike/settings/test-strings/c-1-percent.txt5
-rw-r--r--spike/settings/test-strings/c-2-angle.txt5
-rw-r--r--spike/settings/test-strings/c-3-backslash.txt5
-rw-r--r--spike/settings/test-strings/c-4-quote.txt5
-rw-r--r--spike/settings/test-strings/c-5-braces.txt5
-rw-r--r--spike/settings/test-strings/c-6-latin.txt5
-rw-r--r--spike/settings/test-strings/c-7-emoji.txt5
-rw-r--r--spike/settings/test-strings/c-8-json.txt5
-rw-r--r--spike/settings/test-strings/index.html9
-rw-r--r--spike/settings/test-strings/make_test_strings.py24
14 files changed, 126 insertions, 16 deletions
diff --git a/README.md b/README.md
index 4606c05..f9030ef 100644
--- a/README.md
+++ b/README.md
@@ -402,7 +402,7 @@ Garmin's "array settings" (growable lists) are broken today. A failed save reset
| Property | Type | Field | Notes |
| --- | --- | --- | --- |
-| `impText1` to `impText8` | string | alphaNumeric | Joined in order. The number of fields is set after the settings spike (M1) |
+| `impText1` to `impText8` | string | alphaNumeric | Joined in order. The settings spike (M1) measured 8000 characters per field and 64,000 in total arriving complete |
| `impMode` | number | list | Append (default), Replace imported entries, Cancel pending import |
| `impLocked` | boolean | switch | Require PIN for every entry in this import |
| `impApply` | boolean | switch | **Import now** |
@@ -519,7 +519,8 @@ The full files are in `resources/settings/` (written in M7). `make lint` validat
- **Fields**: the eight import fields are joined in order. If the text is longer than one field accepts, continue in the next one.
- **Several syncs**: if the JSON is still incomplete after joining, the watch keeps what it got (encrypted under the device key) and reports: "Waiting for more: received 3,214 characters. Paste the next part into the import fields and turn 'Import now' on again." "Cancel pending import" discards it. The buffer is capped at 32 KB and expires after 24 hours. M5 measured an Aegis export at about 190 bytes per entry compact, and 400 to 500 pretty-printed. Exports larger than the cap need to be split into several imports.
- **Size reality**: pretty-printed Aegis JSON takes about 400 to 500 characters per entry without icons. Icons add several KB each and are the main reason a paste might not fit. An `otpauth://` URI takes about 80 to 200 characters.
-- **iPhone**: Garmin Connect on iOS reportedly rejects `&` (present in every URI with parameters) and `\` (used in JSON escapes). The settings spike checks this. If it's confirmed, the parser also accepts `;` in place of `&`, and the user guide explains the workaround.
+- **Spike results (M1, Android, 2026-10-21)**: one field carried 8000 characters and the eight together 64,000, so the 32 KB buffer, not Garmin Connect, limits one sync. `otpauth://` URIs with `&` save as they are. A test string mixing `< > \ " { } %`, accented letters and an emoji was refused by the phone; which character blocks it is being narrowed down (docs/SETTINGS_SPIKE.md). If `"` or `{ }` are refused, Aegis JSON can't be pasted and URIs are the import path.
+- **iPhone**: Garmin Connect on iOS reportedly rejects `&` (present in every URI with parameters) and `\` (used in JSON escapes). The spike ran on Android only. If it's confirmed on iOS, the parser also accepts `;` in place of `&`, and the user guide explains the workaround.
### 11.3 Parsing rules
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index 4c35be6..0433457 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -27,6 +27,23 @@ The targets are in [README section 18](../README.md#18-performance-budgets). Thi
## Results
+### Forerunner 70, 2026-10-21 (real watch, debug build 0.8.0)
+
+The first numbers from the real watch, read from "Debug: benchmark" on the sideloaded debug build (docs/YOUR_STEPS.md part 4.2):
+
+| Value | Forerunner 70 | Simulator (M4/M8) | Note |
+| --- | --- | --- | --- |
+| TOTP, SHA-1 | 18 ms | under 1 ms | One code per timer event is far inside the watchdog |
+| TOTP, SHA-512 | 732 ms | 17.5 ms | About 40 times SHA-1, as the simulator ratio predicted. A list with several SHA-512 entries fills in about one per second; the detail screen shows the code within a second |
+| PBKDF2 | 2,000 iterations per second | about 12,000 | Exactly the lower bound in README 18, so setting a PIN calibrates to the 2,000-iteration floor and unlocking takes about 1 s |
+| Header reading | 0 entries in 1 ms | | The benchmark ran before the demo entries were added, so this says nothing yet |
+| Self-test | 292 ms | | The crypto known-answer tests at start-up of the debug build |
+| Memory at the benchmark | 179 of 763 KB (23 %) | 21 % at the first frame | Well under the 70 % budget |
+
+Other timings reported from the same session: settings saved on the phone reach the watch in about 1 second (settings spike, test G). The Member QR demo card took noticeably long on its first opening and about 1 second the second time; its progress display jumped to 90 % almost at once, which is fixed in the next version (the percentage now follows the work, not the phase count).
+
+What this means: unlocking stays at about 1 s because 2,000 iterations is also the minimum the app accepts. An attacker who copies the watch's storage still has to run those 2,000 iterations per guess, but on a PC that is microseconds, so the PIN protects mainly against someone holding the watch (lockout and the optional erase after 10 wrong PINs), not against an offline attack on a copied file system (docs/SECURITY.md).
+
### M8, 2026-10-21 (polish and release candidate, version 0.8.0)
- **Memory, Forerunner 255S (512 KB):**
diff --git a/docs/SETTINGS_SPIKE.md b/docs/SETTINGS_SPIKE.md
index 340670a..a3e2328 100644
--- a/docs/SETTINGS_SPIKE.md
+++ b/docs/SETTINGS_SPIKE.md
@@ -61,23 +61,37 @@ Keep the spike app open on the watch for tests A to H, except where a test says
## Results
-Fill this in (or tell me the values and I'll record them). Date, phone model, Garmin Connect app version (Settings, About) and the watch firmware help explain later differences.
+Run on 2026-10-21 by the project owner with the Store beta of Settings Spike on the Forerunner 70 and an Android phone. The two version numbers were reported as "2.50" and "18.32" without labels; the phone model wasn't recorded.
| Item | Result |
| --- | --- |
-| Date, phone, Garmin Connect version, watch firmware | |
-| A. Longest single field that arrives complete | |
-| A. What happens above that (refused / error / cut to N) | |
-| B. Largest total across the eight fields | |
-| C. Characters that didn't arrive or blocked saving | |
-| D. Status written by the watch visible on the phone (yes/no) | |
-| D. Password field empty after the watch cleared it (yes/no) | |
-| E. Changes arrive while the app is open (yes/no) | |
-| E. Changes arrive while the app is closed, seen at next start (yes/no) | |
-| F. Password field: arrives on the watch, masked on the phone (yes/no each) | |
-| G. Time from Save to the watch updating (short / longest) | |
-| H. Phone-side validation of the number fields (message) | |
-| Other observations | |
+| Date, phone, Garmin Connect version, watch firmware | 2026-10-21, Android phone, versions 2.50 and 18.32 (which is the app and which the firmware is to be confirmed) |
+| A. Longest single field that arrives complete | 8000 characters, the longest string tested |
+| A. What happens above that (refused / error / cut to N) | Not tested: no string longer than 8000 was tried |
+| B. Largest total across the eight fields | 64,000 characters (8 fields of 8000) saved and arrived complete |
+| C. Characters that didn't arrive or blocked saving | The phone **refused to save** the `special` string. An `otpauth://` URI with `&`, `?`, `=`, `:` and `@` saved and arrived. Which of `< > \ " { } %`, the accented letters, `€` or the emoji blocks saving is not known yet: see the follow-up below |
+| D. Status written by the watch visible on the phone (yes/no) | Yes: the phone shows the "seen ... change #N" line and its number goes up |
+| D. Password field empty after the watch cleared it (yes/no) | Yes |
+| E. Changes arrive while the app is open (yes/no) | Yes |
+| E. Changes arrive while the app is closed, seen at next start (yes/no) | Yes |
+| F. Password field: arrives on the watch, masked on the phone (yes/no each) | When the settings were opened again, the field was empty, because the watch clears it after every save (as in D). The "got N chars" line on the watch wasn't reported. The OTP Wallet secret fields, which use the same field type, worked in the real-watch test (YOUR_STEPS 5.2), so the field is usable |
+| G. Time from Save to the watch updating (short / longest) | About 1 second |
+| H. Phone-side validation of the number fields (message) | 301 is rejected by the phone and the settings can't be saved. The switch, list (option C, value 2) and the long number 9999999999 arrive (`switchField: true`, `listField: 2`) |
+| Other observations | None |
+
+### Follow-up for test C
+
+The `special` string mixes many characters, so its failure doesn't say which one the phone rejects. The test-string page now has eight short strings, one per character group (`C1` percent, `C2` angle brackets, `C3` backslash, `C4` double quote, `C5` braces, `C6` accented letters and `€`, `C7` emoji, `C8` a JSON fragment), and `aegis-json`, a complete fake unencrypted Aegis export with one entry. Paste each into Text 2 and save; note which ones the phone refuses. Regenerate the page with `python3 spike/settings/test-strings/make_test_strings.py`.
+
+This decides whether Aegis JSON can be pasted into the import fields at all (it needs `"`, `{`, `}` and sometimes `\`). If the phone refuses those characters, Aegis import goes back to the design table: the fallback is pasting `otpauth://` URIs, which are confirmed to work.
+
+### What the results change
+
+- **Import field size (A, B).** The import design assumed fields of a few hundred characters. One field takes at least 8000 characters and the eight together at least 64,000, so Garmin Connect is not the limit: the watch's 32 KB import buffer is (README 11.2). About 160 compact Aegis entries, or 60 to 80 pretty-printed ones, fit in one sync. The eight `impText` fields and multi-part import stay as they are.
+- **`otpauth://` URIs (C).** They paste as they are, so the parser doesn't need the `;` fallback for `&`.
+- **Write-back (D).** Status lines written by the watch (the forms' **Result** fields) and cleared secret fields show on the phone after reopening the settings.
+- **Processing (E, G).** Changes arrive within about a second while the app is open, and at the next start when it is closed. The app already processes forms on both paths.
+- **Number validation (H).** The phone enforces `min` and `max` itself, so the watch-side range checks are a second line of defence only.
## How the results feed the design
diff --git a/spike/settings/test-strings/aegis-json.txt b/spike/settings/test-strings/aegis-json.txt
new file mode 100644
index 0000000..82b54f2
--- /dev/null
+++ b/spike/settings/test-strings/aegis-json.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: fake unencrypted Aegis export.
+# Copy ONLY the last line (the test string) into a settings field.
+
+{"version":1,"header":{"slots":null,"params":null},"db":{"version":3,"entries":[{"type":"totp","uuid":"00000000-0000-4000-8000-000000000001","name":"alice@example.com","issuer":"TestOne","note":"","icon":null,"info":{"secret":"JBSWY3DPEHPK3PXP","algo":"SHA1","digits":6,"period":30}}]}}
diff --git a/spike/settings/test-strings/c-1-percent.txt b/spike/settings/test-strings/c-1-percent.txt
new file mode 100644
index 0000000..b41a582
--- /dev/null
+++ b/spike/settings/test-strings/c-1-percent.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C1: percent.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C1 % 100% END
diff --git a/spike/settings/test-strings/c-2-angle.txt b/spike/settings/test-strings/c-2-angle.txt
new file mode 100644
index 0000000..3b55be6
--- /dev/null
+++ b/spike/settings/test-strings/c-2-angle.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C2: angle brackets.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C2 < > <b> END
diff --git a/spike/settings/test-strings/c-3-backslash.txt b/spike/settings/test-strings/c-3-backslash.txt
new file mode 100644
index 0000000..d1a2c06
--- /dev/null
+++ b/spike/settings/test-strings/c-3-backslash.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C3: backslash.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C3 \ a\/b END
diff --git a/spike/settings/test-strings/c-4-quote.txt b/spike/settings/test-strings/c-4-quote.txt
new file mode 100644
index 0000000..71c98df
--- /dev/null
+++ b/spike/settings/test-strings/c-4-quote.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C4: double quote.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C4 " "x" END
diff --git a/spike/settings/test-strings/c-5-braces.txt b/spike/settings/test-strings/c-5-braces.txt
new file mode 100644
index 0000000..e529fe8
--- /dev/null
+++ b/spike/settings/test-strings/c-5-braces.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C5: braces.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C5 { } {x} END
diff --git a/spike/settings/test-strings/c-6-latin.txt b/spike/settings/test-strings/c-6-latin.txt
new file mode 100644
index 0000000..d05e7bf
--- /dev/null
+++ b/spike/settings/test-strings/c-6-latin.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C6: accented letters and euro sign.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C6 äöü ß € END
diff --git a/spike/settings/test-strings/c-7-emoji.txt b/spike/settings/test-strings/c-7-emoji.txt
new file mode 100644
index 0000000..ea11b5b
--- /dev/null
+++ b/spike/settings/test-strings/c-7-emoji.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C7: emoji.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C7 😀 END
diff --git a/spike/settings/test-strings/c-8-json.txt b/spike/settings/test-strings/c-8-json.txt
new file mode 100644
index 0000000..b854a91
--- /dev/null
+++ b/spike/settings/test-strings/c-8-json.txt
@@ -0,0 +1,5 @@
+# Author: cirakg & claude ai
+# Summary: Settings spike test string: C8: JSON fragment.
+# Copy ONLY the last line (the test string) into a settings field.
+
+C8 {"name":"Fake","n":1} END
diff --git a/spike/settings/test-strings/index.html b/spike/settings/test-strings/index.html
index d330b52..243cc6b 100644
--- a/spike/settings/test-strings/index.html
+++ b/spike/settings/test-strings/index.html
@@ -18,6 +18,15 @@ button{font-size:1.1em;padding:6px 16px;margin:4px 0 16px}</style></head>
<section><h2>4000 characters</h2><textarea id="len-4000" readonly rows="3">N=4000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgEND</textarea><button onclick="copyText('len-4000')">Copy</button></section>
<section><h2>8000 characters</h2><textarea id="len-8000" readonly rows="3">N=8000|abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP0050rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP0100klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP0150defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP0200wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP0250pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP0300ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP0350bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP0400uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP0450nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP0500ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP0550zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP0600stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP0650lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP0700efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP0750xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP0800qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP0850jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP0900cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP0950vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP1000opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP1050hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP1100abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP1150tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP1200mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP1250fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP1300yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP1350rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP1400klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP1450defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP1500wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP1550pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP1600ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP1650bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP1700uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP1750nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP1800ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP1850zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP1900stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP1950lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP2000efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP2050xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP2100qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP2150jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP2200cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP2250vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP2300opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP2350hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP2400abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP2450tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP2500mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP2550fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP2600yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP2650rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP2700klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP2750defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP2800wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP2850pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP2900ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP2950bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP3000uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP3050nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP3100ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP3150zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP3200stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP3250lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP3300efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP3350xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP3400qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP3450jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP3500cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP3550vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP3600opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP3650hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP3700abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP3750tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP3800mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP3850fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP3900yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP3950rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP4000klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP4050defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP4100wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP4150pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP4200ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP4250bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP4300uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP4350nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP4400ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP4450zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP4500stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP4550lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP4600efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP4650xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP4700qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP4750jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP4800cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP4850vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP4900opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP4950hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP5000abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP5050tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP5100mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP5150fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP5200yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP5250rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP5300klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP5350defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP5400wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP5450pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP5500ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP5550bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP5600uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP5650nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP5700ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP5750zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP5800stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP5850lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP5900efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP5950xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP6000qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP6050jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP6100cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP6150vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP6200opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP6250hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP6300abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP6350tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP6400mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP6450fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP6500yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP6550rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP6600klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP6650defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvP6700wxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnoP6750pqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghP6800ijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzaP6850bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstP6900uvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmP6950nopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefP7000ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyP7050zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrP7100stuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkP7150lmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdP7200efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwP7250xyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopP7300qrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghiP7350jklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabP7400cdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuP7450vwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnP7500opqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefgP7550hijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzP7600abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrsP7650tuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklP7700mnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdeP7750fghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxP7800yzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqP7850rstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijP7900klmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcP7950defghijklmnopqrstuvwxyzabcdefghijklmnopqrsEND</textarea><button onclick="copyText('len-8000')">Copy</button></section>
<section><h2>special characters</h2><textarea id="special" readonly rows="3">special: &amp; &lt; &gt; \ &quot; { } % | non-ASCII: äöü ß € 😀 | otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&amp;issuer=Fake&amp;digits=6 | {&quot;name&quot;:&quot;Fake&quot;,&quot;path&quot;:&quot;a\/b&quot;} END</textarea><button onclick="copyText('special')">Copy</button></section>
+<section><h2>C1: percent</h2><textarea id="c-1-percent" readonly rows="3">C1 % 100% END</textarea><button onclick="copyText('c-1-percent')">Copy</button></section>
+<section><h2>C2: angle brackets</h2><textarea id="c-2-angle" readonly rows="3">C2 &lt; &gt; &lt;b&gt; END</textarea><button onclick="copyText('c-2-angle')">Copy</button></section>
+<section><h2>C3: backslash</h2><textarea id="c-3-backslash" readonly rows="3">C3 \ a\/b END</textarea><button onclick="copyText('c-3-backslash')">Copy</button></section>
+<section><h2>C4: double quote</h2><textarea id="c-4-quote" readonly rows="3">C4 &quot; &quot;x&quot; END</textarea><button onclick="copyText('c-4-quote')">Copy</button></section>
+<section><h2>C5: braces</h2><textarea id="c-5-braces" readonly rows="3">C5 { } {x} END</textarea><button onclick="copyText('c-5-braces')">Copy</button></section>
+<section><h2>C6: accented letters and euro sign</h2><textarea id="c-6-latin" readonly rows="3">C6 äöü ß € END</textarea><button onclick="copyText('c-6-latin')">Copy</button></section>
+<section><h2>C7: emoji</h2><textarea id="c-7-emoji" readonly rows="3">C7 😀 END</textarea><button onclick="copyText('c-7-emoji')">Copy</button></section>
+<section><h2>C8: JSON fragment</h2><textarea id="c-8-json" readonly rows="3">C8 {&quot;name&quot;:&quot;Fake&quot;,&quot;n&quot;:1} END</textarea><button onclick="copyText('c-8-json')">Copy</button></section>
+<section><h2>fake unencrypted Aegis export</h2><textarea id="aegis-json" readonly rows="3">{&quot;version&quot;:1,&quot;header&quot;:{&quot;slots&quot;:null,&quot;params&quot;:null},&quot;db&quot;:{&quot;version&quot;:3,&quot;entries&quot;:[{&quot;type&quot;:&quot;totp&quot;,&quot;uuid&quot;:&quot;00000000-0000-4000-8000-000000000001&quot;,&quot;name&quot;:&quot;alice@example.com&quot;,&quot;issuer&quot;:&quot;TestOne&quot;,&quot;note&quot;:&quot;&quot;,&quot;icon&quot;:null,&quot;info&quot;:{&quot;secret&quot;:&quot;JBSWY3DPEHPK3PXP&quot;,&quot;algo&quot;:&quot;SHA1&quot;,&quot;digits&quot;:6,&quot;period&quot;:30}}]}}</textarea><button onclick="copyText('aegis-json')">Copy</button></section>
<script>
function copyText(id) {
const area = document.getElementById(id);
diff --git a/spike/settings/test-strings/make_test_strings.py b/spike/settings/test-strings/make_test_strings.py
index 8d43977..9632220 100644
--- a/spike/settings/test-strings/make_test_strings.py
+++ b/spike/settings/test-strings/make_test_strings.py
@@ -14,6 +14,11 @@ used, so the length tests don't also test character handling.
special.txt holds the characters that may be rejected (& < > \\ " { } %),
non-ASCII text, a fake otpauth URI and a JSON fragment.
+The phone refused to save special.txt (spike run 2026-10-21), so the c-*.txt
+strings split it into one character group each, and aegis-json.txt is a
+complete fake unencrypted Aegis export: together they show which characters
+block saving and whether an Aegis file can be pasted at all.
+
Usage: python3 make_test_strings.py (writes into its own folder)
"""
@@ -28,6 +33,23 @@ SPECIAL = (
"otpauth://totp/Fake:alice?secret=JBSWY3DPEHPK3PXP&issuer=Fake&digits=6 | "
'{"name":"Fake","path":"a\\/b"} END'
)
+# Test C, split: (file name, label, string). Each ends with " END".
+SPLIT = (
+ ("c-1-percent", "C1: percent", "C1 % 100% END"),
+ ("c-2-angle", "C2: angle brackets", "C2 < > <b> END"),
+ ("c-3-backslash", "C3: backslash", "C3 \\ a\\/b END"),
+ ("c-4-quote", "C4: double quote", 'C4 " "x" END'),
+ ("c-5-braces", "C5: braces", "C5 { } {x} END"),
+ ("c-6-latin", "C6: accented letters and euro sign", "C6 äöü ß € END"),
+ ("c-7-emoji", "C7: emoji", "C7 😀 END"),
+ ("c-8-json", "C8: JSON fragment", 'C8 {"name":"Fake","n":1} END'),
+)
+AEGIS_JSON = (
+ '{"version":1,"header":{"slots":null,"params":null},"db":{"version":3,"entries":['
+ '{"type":"totp","uuid":"00000000-0000-4000-8000-000000000001","name":"alice@example.com",'
+ '"issuer":"TestOne","note":"","icon":null,"info":{"secret":"JBSWY3DPEHPK3PXP",'
+ '"algo":"SHA1","digits":6,"period":30}}]}}'
+)
HEADER = (
"# Author: cirakg & claude ai\n# Summary: {summary}\n"
"# Copy ONLY the last line (the test string) into a settings field.\n\n"
@@ -55,6 +77,8 @@ def main() -> None:
here = Path(__file__).resolve().parent
entries = [(f"len-{n:04d}", f"{n} characters", marker_string(n)) for n in LENGTHS]
entries.append(("special", "special characters", SPECIAL))
+ entries.extend(SPLIT)
+ entries.append(("aegis-json", "fake unencrypted Aegis export", AEGIS_JSON))
for name, label, value in entries:
(here / f"{name}.txt").write_text(HEADER.format(summary=f"Settings spike test string: {label}.") + value + "\n")
cards = "\n".join(