summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rwxr-xr-xdocker/scripts/app-diag.sh2
-rw-r--r--docs/PERFORMANCE.md1
-rw-r--r--source/import/OtpAuth.mc29
-rw-r--r--source/util/StressTest.mc27
5 files changed, 49 insertions, 12 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42571b4..43fd4af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,8 @@ All notable changes to this project are documented here. The format follows
changes shown; later ones keep waiting.
- With storage full, every correct PIN stopped the app while promoting
pending entries; it now reports "Storage full" and continues.
+- Pasting about 100 `otpauth://` links tripped the watchdog: splitting the
+ text looped over every character in one event.
### Tests
diff --git a/docker/scripts/app-diag.sh b/docker/scripts/app-diag.sh
index 773fd51..f1e1fd5 100755
--- a/docker/scripts/app-diag.sh
+++ b/docker/scripts/app-diag.sh
@@ -24,7 +24,7 @@ ciq_require_key
# The stress test takes a few minutes per device (about 3,500 job steps at
# 50 ms on the 512 KB watches).
WATCHDOG_TIMEOUT_S=1200
-STRESS_ITEMS=19
+STRESS_ITEMS=20
if [[ "${kind}" == watchdog ]]; then
for device in "$@"; do
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
index d261125..bf5d97b 100644
--- a/docs/PERFORMANCE.md
+++ b/docs/PERFORMANCE.md
@@ -57,6 +57,7 @@ What this means: unlocking stays at about 1 s because 2,000 iterations is also t
| Replacing a code with 4 KB of contents, queued / direct | not tested | passes, 735 / 727 steps |
| A 4 KB replacement queued by 0.9.0, read, kept at the PIN and confirmed | not tested | passes, 757 steps |
| A 1,024-character secret replacement / 4,096 spaces as code contents | not tested | passes, 10 / 12 steps |
+| Splitting 100 pasted `otpauth://` links (13,576 characters) | trips | passes, one event |
Not covered yet: opening a 4 KB code (`toUtf8` in one event, about 100,000 bytecodes) and the Add OTP form's secret decoding without a length limit; both are near the Forerunner 255S's limit, not the 70's.
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>;