aboutsummaryrefslogtreecommitdiff
path: root/source
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-27 01:08:30 +0100
committercirakg <cirakg@cirakg.xyz>2026-10-27 01:08:30 +0100
commit86c11634379ab87238d794f8f9e8bbc991041e4d (patch)
tree4cf3943528c84205a85a33011d8361f17c2c7638 /source
parentbc4cc19df5f53519ca1aabd3cf161b6d78ffc16c (diff)
fix(import): splitting pasted otpauth:// links runs no character loop
OtpAuth.splitUris/firstWord looped over every character in one event, so pasting about 100 links tripped the watchdog (the UI fixture imports Aegis JSON, so it never ran this path). They now use native searches only. New make watchdog case uris_100 (13,576 characters, twice per event) trips on the old code and passes now. fr70: make lint, make test, make watchdog pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source')
-rw-r--r--source/import/OtpAuth.mc29
-rw-r--r--source/util/StressTest.mc27
2 files changed, 45 insertions, 11 deletions
diff --git a/source/import/OtpAuth.mc b/source/import/OtpAuth.mc
index cdf0f9c..f5c487b 100644
--- a/source/import/OtpAuth.mc
+++ b/source/import/OtpAuth.mc
@@ -21,28 +21,35 @@ module OtpAuth {
const MIGRATION_PREFIX = "otpauth-migration://";
// The URIs in text, split at every "otpauth://". Text before the first
- // one is ignored here; the importer reports it.
+ // one is ignored here; the importer reports it. Runs in one event, so
+ // it uses only native searches: a VM loop over every character of 100
+ // pasted links tripped the watchdog.
function splitUris(text as String) as Array<String> {
var uris = [] as Array<String>;
- var at = Text.findIgnoreCase(text, PREFIX, 0);
- while (at >= 0) {
- var next = Text.findIgnoreCase(text, PREFIX, at + PREFIX.length());
- var end = next >= 0 ? next : text.length();
- uris.add(firstWord(text.substring(at, end) as String));
+ var lower = text.toLower();
+ var at = lower.find(PREFIX);
+ while (at != null) {
+ var from = at + PREFIX.length();
+ var found = (lower.substring(from, lower.length()) as String).find(PREFIX);
+ var next = found != null ? from + found : null;
+ uris.add(firstWord(text.substring(at, next != null ? next : text.length()) as String));
at = next;
}
return uris;
}
// Text up to the first whitespace (a pasted URI never contains any).
+ // Native searches, no loop over the characters (see splitUris).
function firstWord(text as String) as String {
- var chars = text.toCharArray();
- for (var i = 0; i < chars.size(); i++) {
- if (Text.isSpace(chars[i])) {
- return text.substring(0, i) as String;
+ var end = text.length();
+ var spaces = [" ", "\t", "\n", "\r"];
+ for (var i = 0; i < spaces.size(); i++) {
+ var found = text.find(spaces[i]);
+ if (found != null && found < end) {
+ end = found;
}
}
- return text;
+ return end == text.length() ? text : text.substring(0, end) as String;
}
// One URI as an import item (entry, problem or skip).
diff --git a/source/util/StressTest.mc b/source/util/StressTest.mc
index a9d9293..8956c4b 100644
--- a/source/util/StressTest.mc
+++ b/source/util/StressTest.mc
@@ -130,6 +130,10 @@ module StressTest {
// queued (ManageRun), then confirmed (ChangeQueueJob).
// * SPACES: an unlocked code entry and a REPLACE of `material` spaces
// (refused: "Contents: missing").
+ // * URIS: no vault. Setup builds 100 pasted otpauth:// links (about 150
+ // characters each) in steps of ten; the measured run is the
+ // Importer's text work in one event, done twice: OtpAuth.splitUris
+ // and the detect step's Text.trim + Text.findIgnoreCase.
class VaultCase extends Jobs.Job {
static const UNLOCK = 0;
static const COUNT = 1;
@@ -139,6 +143,8 @@ module StressTest {
static const LEGACY = 5;
static const SECRET = 6;
static const SPACES = 7;
+ static const URIS = 8;
+ static const LINKS = 100;
// Unlocked entries, one queued item each (ChangeQueue.MAX_ITEMS).
static const LOCKS = 16;
static const PIN = "1234";
@@ -167,6 +173,8 @@ module StressTest {
private var _done as Array<Boolean> = [] as Array<Boolean>;
// LEGACY: the keys each vault's menu read.
private var _keys as Array<Array<String>> = [] as Array<Array<String>>;
+ // URIS: the pasted text.
+ private var _text as String = "";
function initialize(label as String, type as Number, pendingCount as Number, materialBytes as Number) {
Job.initialize();
@@ -198,6 +206,16 @@ module StressTest {
// One bounded setup step; true when every vault is ready.
private function setup(budget as Number) as Boolean {
+ if (kind == URIS) {
+ var secret = repeat("ABCDEFGH", 4);
+ for (var i = 0; i < 10; i++) {
+ var n = (_n + 1).toString();
+ _text += "otpauth://totp/Issuer" + n + ":account" + n + "@example.com?secret=" + secret
+ + "&issuer=Issuer" + n + "&algorithm=SHA1&digits=6&period=30\n";
+ _n++;
+ }
+ return _n >= LINKS;
+ }
var v = _vaults.size() > 0 ? _vaults[_vaults.size() - 1] : null;
if (_phase == 0) {
v = new Vault(new RamBackend(), new TimeSource());
@@ -340,6 +358,14 @@ module StressTest {
// The measured run.
private function measure(budget as Number) as Boolean {
+ if (kind == URIS) {
+ for (var i = 0; i < MARGIN; i++) {
+ var trimmed = Text.trim(_text);
+ var first = Text.findIgnoreCase(trimmed, OtpAuth.MIGRATION_PREFIX, 0) + Text.findIgnoreCase(trimmed, OtpAuth.PREFIX, 0);
+ result = " uris=" + OtpAuth.splitUris(trimmed).size() + " chars=" + _text.length() + " first=" + first;
+ }
+ return true;
+ }
if (kind == COUNT) {
for (var i = 0; i < MARGIN; i++) {
ChangeQueue.changed();
@@ -508,6 +534,7 @@ module StressTest {
new VaultCase("queue_legacy_4k", VaultCase.LEGACY, 0, Entry.MAX_PAYLOAD_BYTES),
new VaultCase("replace_secret_1k", VaultCase.SECRET, 0, 0),
new VaultCase("replace_spaces_4k", VaultCase.SPACES, 0, Entry.MAX_PAYLOAD_BYTES),
+ new VaultCase("uris_100", VaultCase.URIS, 0, 0),
// Last, so it stays on the screen: app-diag.sh decodes it.
new Item("qr_fit", Codes.QR, new Qr.QrJob(SCREEN_TEXT, 1, largestQrVersion(w, h, round), -1))
] as Array<Item or VaultCase>;