diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-20 01:51:05 +0200 |
| commit | 42f9331e693f5058fcfcd7aef5a92cabc04c20ee (patch) | |
| tree | f3dab2657eb592ee468e6cebc9b47d9da7e08683 | |
| parent | d537f4e154a40075405b0ab2fed14fbe2eeaac81 (diff) | |
test(import): cover parsers, importer and a seeded fuzz corpus; import 100 entries in the UI flow
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
23 files changed, 1050 insertions, 5 deletions
@@ -72,7 +72,7 @@ DOCS_RUN = $(DOCKER) run --rm --init --label project=$(PROJECT) \ --read-only --tmpfs /tmp:rw,exec,nosuid,size=1g --shm-size 512m \ -v "$(ROOT)":/work -w /work --network none $(DOCS_IMAGE) -.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors spike spike-sim \ +.PHONY: help image docs-image sdk key build test test-tools sim screenshots screenshots-approve perf memtest icons vectors corpus spike spike-sim \ lint package docs ci shell clean nuke lock products \ check-docker dirs need-image need-docs-image @@ -136,6 +136,9 @@ products: dirs need-image ## Check manifest products against the downloaded devi vectors: dirs need-image ## Regenerate test/data/Vectors.mc (cross-checked RFC/NIST test vectors) $(DEV_OFFLINE) python3 tools/gen_vectors.py +corpus: dirs need-image ## Regenerate test/data/FuzzCorpus.mc (seeded fuzz inputs for the import parsers) + $(DEV_OFFLINE) python3 tools/gen_fuzz_corpus.py + spike: dirs need-image ## Build the M1 settings spike (debug .prg + beta .iq) into build/spike $(DEV_OFFLINE) build-spike.sh build diff --git a/test/data/FuzzCorpus.mc b/test/data/FuzzCorpus.mc new file mode 100644 index 0000000..57bdf8b --- /dev/null +++ b/test/data/FuzzCorpus.mc @@ -0,0 +1,115 @@ +// Author: cirakg & claude ai +// Summary: Seeded fuzz inputs for the import parsers (generated by tools/gen_fuzz_corpus.py; do not edit). +// +// Seed 4242. Hex because many inputs are not valid UTF-8. Fake data only. + +import Toybox.Lang; + +(:test) +module FuzzCorpus { + const JSON = [ + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f74705c75222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223aa07b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20375d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e7977616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2256acb676657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c20226469676974", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e6731225d7d2c207b2274797070222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147456e756c6c5a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c20223d656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c90f620226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c20226f756e746572223a20377d7d5d726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354510f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a2241414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412251222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f222a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c20226122534841323536222c2022646967697422534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c202261676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223d3a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d6522", // lint: allow-literal + "7b22766572224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412273696f6e223a20312c2022686561646572223a207b", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a444754514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c20225c7564383364686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c202269737375655b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b72223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c20225b5b5b5b5b5b5b5b5b5b5b5b69636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c202269756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c20", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e4257615647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147415a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d2241412241414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414122414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141225d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c202264622241414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412241414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141415141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414122223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c20226e756c6c706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a205b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b22616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e42564759333a54514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470225c75643833642c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563626574223a202247455a593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2ce42a20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f22223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c2022753a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657220312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253485b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b4131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275755b5b5b5b5b5b5b5b5b5b5b5b5b5b6964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d7d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205a7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a202247695b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b74487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269de9e047a636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e3d616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a3a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222ce0e51b20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a5122", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022692d897373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223d3a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c203c5b8a4122656e7472696573223a225b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f7570735b5b5b5b5b5b5b5b5b5b223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64222a3a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022645b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b6967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c5d202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f74702224202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a20224769744875627465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f6563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636b6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b227479706522322022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f6e756c6c72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a4447", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c476f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f22223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a444722414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141224e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c205b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b7b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c2022673d726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414122696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b2267202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756570223a202256504e222c207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022646967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d", // lint: allow-literal + "7b2276657273696f6e223a20312c2022686561646572223a207b22736c6f7473223a206e756c6c2c2022706172616d73223a206e756c6c7d2c20226462223a207b2276657273696f6e223a20332c2022656e7472696573223a205b7b2274797065223a2022746f7470222c202275756964223a20227531222c20226e616d65223a2022616c696365222c2022697373756572223a2022476974487562222c20226e6f7465223a2022222c20226661766f72697465223a20747275652c202269636f6e223a206e756c6c2c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a202253484131222c2022646967697473223a20362c2022706572696f64223a2033307d2c202267726f757073223a205b226731225d7d2c207b2274797065223a2022686f7470222c20226e616d65223a2022625c753030653962222c2022697373756572223a202256504e222c2022696e666f223a207b22736563726574223a202247455a44474e425647593354514f4a5147455a44474e425647593354514f4a51222c2022616c676f223a2022534841323536222c2022642241414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141226967697473223a20382c2022706572696f64223a2033302c2022636f756e746572223a20377d7d5d2c202267726f757073223a205b7b2275756964223a20226731222c20226e616d65223a2022576f726b227d5d7d7d" // lint: allow-literal + ]; + + const URIS = [ + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f726974686d3d535841323536", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44574e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d4769746973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f736526637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f726974686d3d534841224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141", // lint: allow-literal + "6f747061", // lint: allow-literal + "6f7470613a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f5a44474e4256475933544a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266365723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f7470b8617574683a2f2f747f74702f476974", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534302c6578616d706c652e636f6d3f7365637265743d47455aac0b44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562", // lint: allow-literal + "6f7470617525fb737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414122706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e42564759224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141223354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c252e636f6d3f7365637265743d47455a44474e4256475933545111e8b9004f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "", // lint: allow-literal + "6f7470617574683a2f2f746f7470703bd42f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f686f74623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f747244af70617574683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a6144474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e42564f4a5147455a44474e425647593354514f4a224141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412251", // lint: allow-literal + "6f7470617574683ae0e32f2f737465616d2f6561", // lint: allow-literal + "6f747061757422683a2f2f73683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f736563723a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f22414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141afed4141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141224a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f742d70617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f7269746e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44475b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b4e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e4256475933455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973a3b5737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d4745", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f72697468593354514f4a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f747061757468cda2736f3a2f2f737465616d2f537465616d3a6361726f6c3f73656372228bb7eb65743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a5126636f756e475147455a44474e425647593356514f4a5126636f756e47593354514f4a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c69636525345b5b5b5b5b5b5b5b5b5b5b5b5b306578616d706c652e636f6d3f7365637265743d47455a", // lint: allow-literal + "6f5b5b5b5b5b4b5b5b5b5b5b5b5b5b747061", // lint: allow-literal + "6f2f2f686f74702f56504e3a626f623f7365637265743d47455a44474e425647593354514f4a514745", // lint: allow-literal + "6f7470617574683a2f2f686f74702f56504e3a626f623f7365637265743d47455a44474efb7d00425647593354514f4a5147455a44474e425647593354514f4a5126636f756e7465723d353b616c676f726974686d3d5348413235364a5126636f756e7465723d353b616c676f726974686d3d534841323536", // lint: allow-literal + "6f74706175e87b49f374683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a5c44474e425647593354514f4a51266973737565", // lint: allow-literal + "6f7470617574683a", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c6571032e636f6d652e636f6d", // lint: allow-literal + "6f7470617574683aaf2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f742241414141414141414141414141414141414141414141414141414141", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636e5b5b5b5b5b5b5b5b5b5b5b6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a51475d455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f5b5b5b5b5b5b8956945b5b5b5b5b5b5b5b5b5b5b5b5b737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a514745", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d475b5b5b5b5b5b5b5b5b966dcec25b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b5b455a44474e425647593354514f4a5147455a44474e425647593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a616c6963652534306578616d706c652e636f6d3f7365637265743d47455a44474e4256471b3354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d366974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f537465616d3a6361726f6c3a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e42564759223354514f4a51", // lint: allow-literal + "22414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141226f", // lint: allow-literal + "6f7470617574683a2f2f746f74702f4769744875623a4769744875623a616c69636525343065785c616d706c652e636f6d3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51266973737565723d476974487562266469676974733d36", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f537465616d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e4256477b593354514f4a51", // lint: allow-literal + "6f7470617574683a2f2f737465616d2f5374656154334c7d6d3a6361726f6c3f7365637265743d47455a44474e425647593354514f4a5147455a44474e425647593354514f4a51" // lint: allow-literal + ]; +} diff --git a/test/golden/fenix7/about.png b/test/golden/fenix7/about.png Binary files differindex d6a87a2..3f74946 100644 --- a/test/golden/fenix7/about.png +++ b/test/golden/fenix7/about.png diff --git a/test/golden/fenix7/import_results.png b/test/golden/fenix7/import_results.png Binary files differnew file mode 100644 index 0000000..34c1cf7 --- /dev/null +++ b/test/golden/fenix7/import_results.png diff --git a/test/golden/fr255s/about.png b/test/golden/fr255s/about.png Binary files differindex f8f23d2..b2f2548 100644 --- a/test/golden/fr255s/about.png +++ b/test/golden/fr255s/about.png diff --git a/test/golden/fr255s/import_results.png b/test/golden/fr255s/import_results.png Binary files differnew file mode 100644 index 0000000..d2cdc19 --- /dev/null +++ b/test/golden/fr255s/import_results.png diff --git a/test/golden/fr70/about.png b/test/golden/fr70/about.png Binary files differindex c46fa11..d7d18a0 100644 --- a/test/golden/fr70/about.png +++ b/test/golden/fr70/about.png diff --git a/test/golden/fr70/import_results.png b/test/golden/fr70/import_results.png Binary files differnew file mode 100644 index 0000000..2da3dcc --- /dev/null +++ b/test/golden/fr70/import_results.png diff --git a/test/golden/fr965/about.png b/test/golden/fr965/about.png Binary files differindex 61b9745..65e3902 100644 --- a/test/golden/fr965/about.png +++ b/test/golden/fr965/about.png diff --git a/test/golden/fr965/import_results.png b/test/golden/fr965/import_results.png Binary files differnew file mode 100644 index 0000000..c827d20 --- /dev/null +++ b/test/golden/fr965/import_results.png diff --git a/test/golden/instinct3amoled45mm/about.png b/test/golden/instinct3amoled45mm/about.png Binary files differindex 1400601..75ad5de 100644 --- a/test/golden/instinct3amoled45mm/about.png +++ b/test/golden/instinct3amoled45mm/about.png diff --git a/test/golden/instinct3amoled45mm/import_results.png b/test/golden/instinct3amoled45mm/import_results.png Binary files differnew file mode 100644 index 0000000..0dbf559 --- /dev/null +++ b/test/golden/instinct3amoled45mm/import_results.png diff --git a/test/golden/venusq2/import_results.png b/test/golden/venusq2/import_results.png Binary files differnew file mode 100644 index 0000000..647ca21 --- /dev/null +++ b/test/golden/venusq2/import_results.png diff --git a/test/golden/venux1/import_results.png b/test/golden/venux1/import_results.png Binary files differnew file mode 100644 index 0000000..1d1251d --- /dev/null +++ b/test/golden/venux1/import_results.png diff --git a/test/import/AegisTest.mc b/test/import/AegisTest.mc new file mode 100644 index 0000000..eaad039 --- /dev/null +++ b/test/import/AegisTest.mc @@ -0,0 +1,128 @@ +// Author: cirakg & claude ai +// Summary: Tests for the Aegis reader: database versions 1-3, types, parameters, groups, icons, encryption and bad input. + +import Toybox.Lang; +import Toybox.Test; + +// Database version 3: types, algorithms, digits, periods, groups listed +// after the entries, favourites, icons and Unicode. +(:test) +function aegisReadsDatabaseVersion3(logger as Test.Logger) as Boolean { + var icon = ""; + for (var i = 0; i < 200; i++) { + icon += "iVBORw0KGgoAAAANSUhEUgAAA"; + } + var entries = [ + AegisFixtures.entry("totp", "GitHub", "alice", AegisFixtures.SECRET_1, "SHA1", 6, 30, + "\"icon\": \"" + icon + "\", \"icon_mime\": \"image/png\", \"groups\": [\"g-work\", \"g-other\"], "), + AegisFixtures.entry("totp", "Bänk €", "bob", AegisFixtures.SECRET_2, "SHA256", 8, 60, "\"favorite\": true, "), + "{\"type\": \"hotp\", \"name\": \"carol\", \"issuer\": \"VPN\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_3 + + "\", \"algo\": \"SHA512\", \"digits\": 7, \"period\": 30, \"counter\": 12}}", + AegisFixtures.entry("steam", "Steam", "dave", AegisFixtures.SECRET_1, "SHA1", 5, 30, ""), + AegisFixtures.entry("motp", "Old", "erin", AegisFixtures.SECRET_1, "MD5", 6, 10, ""), + AegisFixtures.entry("yandex", "Ya", "frank", AegisFixtures.SECRET_1, "SHA256", 8, 30, ""), + AegisFixtures.entry("totp", "Odd", "gina", AegisFixtures.SECRET_1, "SHA1", 6, 20, "\"groups\": [\"g-missing\"], "), + AegisFixtures.entry("totp", "Slow", "hal", AegisFixtures.SECRET_1, "SHA1", 6, 50, "") + ]; + var groups = "[{\"uuid\": \"g-work\", \"name\": \"Work\"}, {\"uuid\": \"g-other\", \"name\": \"Other\"}]"; + var reader = AegisFixtures.read(AegisFixtures.export(3, entries, groups)); + if (reader.error != null || reader.items.size() != 8) { + logger.error("error " + reader.error + ", items " + reader.items.size()); + return false; + } + var it = reader.items; + var github = it[0].entry; + var bank = it[1].entry; + var vpn = it[2].entry; + var steam = it[3].entry; + var odd = it[6].entry; + var slow = it[7].entry; + if (github == null || bank == null || vpn == null || steam == null || odd == null || slow == null) { + for (var i = 0; i < it.size(); i++) { + logger.error("item " + i + ": " + it[i].problem + " / " + it[i].skipped); + } + return false; + } + var checks = [ + github.name.equals("GitHub") && github.account.equals("alice"), + github.group.equals("Work"), + bank.name.equals("Bänk €") && bank.algorithm == Hashes.SHA256 && bank.digits == 8 && bank.period == 60, + bank.favorite, + vpn.type == Entry.OTP_HOTP && vpn.counter == 12l && vpn.digits == 7 && vpn.algorithm == Hashes.SHA512, + steam.type == Entry.OTP_STEAM, + it[4].skipped != null && it[5].skipped != null, + odd.group.equals("") && odd.period == 20 && slow.period == 50, + Bytes.equal(github.secret as ByteArray, Bytes.fromUtf8("12345678901234567890")) + ]; + for (var i = 0; i < checks.size(); i++) { + if (!(checks[i] as Boolean)) { + logger.error("check " + i + " failed"); + return false; + } + } + return true; +} + +// Database versions 1 and 2 name the group directly. +(:test) +function aegisReadsOlderDatabaseVersions(logger as Test.Logger) as Boolean { + for (var v = 1; v <= 2; v++) { + var reader = AegisFixtures.read(AegisFixtures.export(v, + [AegisFixtures.entry("totp", "Mail", "ann", AegisFixtures.SECRET_1, "SHA1", 6, 30, "\"group\": \"Private\", ")], "")); + var e = reader.items.size() == 1 ? reader.items[0].entry : null; + if (e == null || !e.group.equals("Private")) { + logger.error("database version " + v + " failed: " + reader.error); + return false; + } + } + return true; +} + +(:test) +function aegisRefusesEncryptedExports(logger as Test.Logger) as Boolean { + var slots = "{\"version\": 1, \"header\": {\"slots\": [{\"type\": 1}], \"params\": {}}, \"db\": \"c2VjcmV0\"}"; + var dbString = "{\"version\": 1, \"header\": {\"slots\": null}, \"db\": \"c2VjcmV0\"}"; + var a = AegisFixtures.read(slots); + var b = AegisFixtures.read(dbString); + return a.error != null && (a.error as String).find("encrypted") != null + && b.error != null && (b.error as String).find("encrypted") != null; +} + +(:test) +function aegisReportsBadEntriesAndDocuments(logger as Test.Logger) as Boolean { + var entries = [ + AegisFixtures.entry("totp", "Nine", "x", AegisFixtures.SECRET_1, "SHA1", 9, 30, ""), + "{\"type\": \"totp\", \"name\": \"dup\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_1 + "\", \"secret\": \"" + + AegisFixtures.SECRET_2 + "\", \"algo\": \"SHA1\", \"digits\": 6, \"period\": 30}}", + AegisFixtures.entry("totp", "Bad", "y", "NOT-BASE32!", "SHA1", 6, 30, ""), + "{\"type\": \"totp\", \"name\": \"z\", \"info\": {\"secret\": \"" + AegisFixtures.SECRET_1 + "\", \"digits\": 6.5}}" + ]; + var reader = AegisFixtures.read(AegisFixtures.export(3, entries, "")); + var it = reader.items; + if (reader.error != null || it.size() != 4) { + logger.error("error " + reader.error); + return false; + } + var problems = [it[0].problem, it[1].problem, it[2].problem, it[3].problem]; + var needles = ["9 digits", "twice", "Base32", "whole number"]; + for (var i = 0; i < 4; i++) { + var p = problems[i]; + if (p == null || (p as String).find(needles[i]) == null) { + logger.error("entry " + i + ": " + p); + return false; + } + } + var notJson = AegisFixtures.read("{\"version\": 1, \"db\": [1,]}"); + var notAegis = AegisFixtures.read("{\"hello\": \"world\"}"); + var newer = AegisFixtures.read("{\"version\": 2, \"db\": {\"version\": 3, \"entries\": []}}"); + var deep = "{\"version\": 1, \"x\": " + "[[[[[[[[[[[[[[[[[[1]]]]]]]]]]]]]]]]]]" + "}"; + return notJson.error != null && notAegis.error != null && newer.error != null + && (newer.error as String).find("version 2") != null && AegisFixtures.read(deep).error != null; +} + +(:test) +function aegisSaysWhenTheTextIsIncomplete(logger as Test.Logger) as Boolean { + var full = AegisFixtures.export(3, [AegisFixtures.totp("A", "a", AegisFixtures.SECRET_1), AegisFixtures.totp("B", "b", AegisFixtures.SECRET_2)], ""); + return AegisFixtures.status(full.substring(0, full.length() / 2) as String) == AegisReader.INCOMPLETE + && AegisFixtures.status(full) == AegisReader.DONE; +} diff --git a/test/import/FuzzTest.mc b/test/import/FuzzTest.mc new file mode 100644 index 0000000..7573ffa --- /dev/null +++ b/test/import/FuzzTest.mc @@ -0,0 +1,107 @@ +// Author: cirakg & claude ai +// Summary: Runs the seeded fuzz corpus through the tokenizer, the Aegis reader and the importer: always a result or a typed error, never a crash. +// +// README 17.3. Each input must end in a definite state within a bounded +// number of steps (no endless loop), and anything the importer stores must +// be a valid entry. Watchdog limits aren't enforced in unit tests; the +// per-step bounds are covered by the UI flow on the Forerunner 255S. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +function fuzzTokenizerAlwaysTerminates(logger as Test.Logger) as Boolean { + var inputs = FuzzCorpus.JSON; + for (var i = 0; i < inputs.size(); i++) { + var data = Bytes.fromHex(inputs[i] as String); + var tok = new Json.Tokenizer(data); + var finished = false; + // Every token consumes at least one byte, so size + 2 calls suffice. + for (var n = 0; n < data.size() + 2; n++) { + var t = tok.next(); + if (t == Json.T_END || t == Json.T_INCOMPLETE || t == Json.T_ERROR) { + finished = true; + break; + } + } + if (!finished) { + logger.error("tokenizer didn't finish on input " + i); + return false; + } + } + return true; +} + +(:test) +function fuzzAegisReaderGivesDefiniteResults(logger as Test.Logger) as Boolean { + var inputs = FuzzCorpus.JSON; + var done = 0; + for (var i = 0; i < inputs.size(); i++) { + var data = Bytes.fromHex(inputs[i] as String); + var reader = new AegisReader(data); + var status = AegisReader.RUNNING; + for (var n = 0; n < data.size() + 2 && status == AegisReader.RUNNING; n++) { + status = reader.step(); + } + if (status == AegisReader.RUNNING || (status == AegisReader.FAILED && reader.error == null)) { + logger.error("input " + i + ": status " + status); + return false; + } + if (status == AegisReader.DONE) { + done++; + for (var k = 0; k < reader.items.size(); k++) { + var item = reader.items[k]; + var e = item.entry; + if (e == null ? item.problem == null && item.skipped == null : e.validate() != null) { + logger.error("input " + i + " item " + k + " is neither valid nor explained"); + return false; + } + } + } + } + logger.debug("FUZZ aegis inputs=" + inputs.size() + " complete=" + done); + return done > 0; +} + +// The whole importer on every input that is valid text: a report that +// explains everything, and only valid entries in the vault. +(:test) +function fuzzImporterNeverStoresInvalidEntries(logger as Test.Logger) as Boolean { + var all = [] as Array<String>; + all.addAll(FuzzCorpus.JSON as Array<String>); + all.addAll(FuzzCorpus.URIS as Array<String>); + var imported = 0; + for (var i = 0; i < all.size(); i++) { + var text = Bytes.toUtf8(Bytes.fromHex(all[i])); + if (text == null) { + continue; + } + var vault = new Vault(new MemoryBackend(), new FakeTime()); + var job = new Importer(vault, text, Importer.MODE_APPEND, false, 1800000000l); + var finished = false; + for (var n = 0; n < 5000; n++) { + if (job.step(1)) { + finished = true; + break; + } + } + var r = job.report; + if (!finished || (r.failure == null && r.waitingChars < 0 && r.summary().find("Imported") != 0)) { + logger.error("input " + i + " didn't finish cleanly: " + r.summary()); + return false; + } + var ids = vault.ids(); + for (var k = 0; k < ids.size(); k++) { + var e = vault.open(ids[k]); + var problem = e.validate(); + e.wipeSecrets(); + if (problem != null) { + logger.error("input " + i + " stored an invalid entry: " + problem); + return false; + } + } + imported += ids.size(); + } + logger.debug("FUZZ importer inputs=" + all.size() + " entries_stored=" + imported); + return imported > 0; +} diff --git a/test/import/ImportSupport.mc b/test/import/ImportSupport.mc new file mode 100644 index 0000000..ba891a7 --- /dev/null +++ b/test/import/ImportSupport.mc @@ -0,0 +1,76 @@ +// Author: cirakg & claude ai +// Summary: Builders for fake Aegis exports and otpauth text used by the import tests (all secrets are RFC test keys). + +import Toybox.Lang; +import Toybox.Test; + +(:test) +module AegisFixtures { + + const SECRET_1 = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // lint: allow-literal (RFC 4226 key "12345678901234567890") + const SECRET_2 = "JBSWY3DPEHPK3PXP"; // lint: allow-literal (the classic "Hello!" demo key) + const SECRET_3 = "MFRGGZDFMZTWQ2LKNNWG23TPOBYXE43U"; // lint: allow-literal (Base32 of "abcdefghijklmnopqrst") + + // One Aegis entry. extra is inserted verbatim (other fields, icon...). + function entry(type as String, issuer as String, name as String, secret as String, algo as String, digits as Number, + period as Number, extra as String) as String { + return "{\"type\": \"" + type + "\", \"uuid\": \"0000\", \"name\": \"" + name + "\", \"issuer\": \"" + issuer + + "\", \"note\": \"private note\", " + extra + + "\"info\": {\"secret\": \"" + secret + "\", \"algo\": \"" + algo + "\", \"digits\": " + digits + + ", \"period\": " + period + "}}"; + } + + function totp(issuer as String, name as String, secret as String) as String { + return entry("totp", issuer, name, secret, "SHA1", 6, 30, ""); + } + + // A whole unencrypted export (vault version 1) of the given database + // version, with entries joined by commas and groups after the entries. + function export(dbVersion as Number, entries as Array<String>, groups as String) as String { + var joined = ""; + for (var i = 0; i < entries.size(); i++) { + joined += (i > 0 ? ",\n" : "") + entries[i]; + } + return "{\"version\": 1, \"header\": {\"slots\": null, \"params\": null}, \"db\": {\"version\": " + dbVersion + + ", \"entries\": [\n" + joined + "\n]" + (groups.length() > 0 ? ", \"groups\": " + groups : "") + "}}"; + } + + // Runs the reader to the end (or a non-running status). + function read(text as String) as AegisReader { + var reader = new AegisReader(Bytes.fromUtf8(text)); + for (var i = 0; i < 10000; i++) { + if (reader.step() != AegisReader.RUNNING) { + return reader; + } + } + return reader; + } + + function status(text as String) as Number { + var reader = new AegisReader(Bytes.fromUtf8(text)); + for (var i = 0; i < 10000; i++) { + var s = reader.step(); + if (s != AegisReader.RUNNING) { + return s; + } + } + return -1; + } + + // Runs an import to completion on vault and returns its report. + function runImport(vault as Vault, text as String, mode as Number, locked as Boolean, now as Long) as ImportReport { + var job = new Importer(vault, text, mode, locked, now); + Jobs.runToCompletion(job, 1); + return job.report; + } + + // Some detail line contains needle. + function hasDetail(report as ImportReport, needle as String) as Boolean { + for (var i = 0; i < report.details.size(); i++) { + if (report.details[i].find(needle) != null) { + return true; + } + } + return false; + } +} diff --git a/test/import/ImporterTest.mc b/test/import/ImporterTest.mc new file mode 100644 index 0000000..411e528 --- /dev/null +++ b/test/import/ImporterTest.mc @@ -0,0 +1,208 @@ +// Author: cirakg & claude ai +// Summary: Tests for the import job: reports, duplicates, replacements, locking, Replace mode, multi-part buffer, failures, a full store, 100 entries. + +import Toybox.Lang; +import Toybox.System; +import Toybox.Test; + +(:test) +module ImporterFixtures { + + const NOW = 1800000000l; + + function uri(issuer as String, account as String, secret as String) as String { + return "otpauth://totp/" + issuer + ":" + account + "?secret=" + secret; + } + + function vault() as Vault { + return new Vault(new MemoryBackend(), new FakeTime()); + } +} + +(:test) +function importerStoresUrisAndReports(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var text = ImporterFixtures.uri("GitHub", "alice", AegisFixtures.SECRET_1) + "\n" + + ImporterFixtures.uri("Mail", "bob", AegisFixtures.SECRET_2) + "\n" + + "otpauth://totp/Bad:carol?secret=" + AegisFixtures.SECRET_3 + "&digits=9\n" + + "otpauth://motp/Old:dan?secret=" + AegisFixtures.SECRET_3; + var report = AegisFixtures.runImport(vault, text, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var ids = vault.ids(); + var first = vault.open(ids[0]); + var ok = first.name.equals("GitHub") && first.source == Entry.SOURCE_IMPORT && first.created == ImporterFixtures.NOW; + first.wipeSecrets(); + var summary = report.summary(); + if (!summary.equals("Imported 2 · 1 error · 1 skipped")) { + logger.error("summary: " + summary); + return false; + } + return ok && ids.size() == 2 && AegisFixtures.hasDetail(report, "URI 3 'Bad': 9 digits") + && AegisFixtures.hasDetail(report, "URI 4 'Old': skipped, mOTP"); +} + +// The same type and secret is a duplicate, also of a locked entry while the +// watch is locked (fingerprints); same issuer and account with another +// secret is imported and flagged. +(:test) +function importerFindsDuplicatesAndReplacements(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + StorageFixtures.setPin(vault, StorageFixtures.PIN); + var locked = StorageFixtures.totp("Vault"); + locked.secret = Bytes.fromUtf8("12345678901234567890"); + locked.algorithm = Hashes.SHA1; + locked.locked = true; + var lockedId = vault.add(locked); + vault.lock(); + var first = AegisFixtures.runImport(vault, ImporterFixtures.uri("GitHub", "alice", AegisFixtures.SECRET_2), + Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var text = ImporterFixtures.uri("GitHub", "alice", AegisFixtures.SECRET_2) + " " // duplicate of the first import + + ImporterFixtures.uri("Other", "x", AegisFixtures.SECRET_1) + " " // duplicate of the locked entry + + ImporterFixtures.uri("GitHub", "alice", AegisFixtures.SECRET_3) + " " // same names, new secret + + ImporterFixtures.uri("Twice", "y", AegisFixtures.SECRET_3); // duplicate within this import + var second = AegisFixtures.runImport(vault, text, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + if (second.duplicates != 3 || second.imported != 1) { + logger.error(second.summary()); + return false; + } + return first.imported == 1 && AegisFixtures.hasDetail(second, "duplicate of " + lockedId) + && AegisFixtures.hasDetail(second, "possible replacement for") && vault.ids().size() == 3; +} + +// "Require PIN" locks the whole import; while locked they wait as pending. +(:test) +function importerLocksEntriesAsPending(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + StorageFixtures.setPin(vault, StorageFixtures.PIN); + vault.lock(); + var report = AegisFixtures.runImport(vault, ImporterFixtures.uri("Bank", "eve", AegisFixtures.SECRET_1), + Importer.MODE_APPEND, true, ImporterFixtures.NOW); + return report.imported == 1 && report.pendingLocked == 1 && vault.pendingIds().size() == 1 && vault.ids().size() == 0 + && report.summary().find("waiting for your PIN") != null; +} + +// Replace mode deletes earlier imported entries, never manual ones. +(:test) +function importerReplaceModeKeepsManualEntries(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var manualId = vault.add(StorageFixtures.totp("Manual")); + // (The manual fixture uses SECRET_1's bytes, so the imports use other secrets.) + AegisFixtures.runImport(vault, ImporterFixtures.uri("Old", "a", AegisFixtures.SECRET_2) + " " + + ImporterFixtures.uri("Older", "b", AegisFixtures.SECRET_3), Importer.MODE_APPEND, false, ImporterFixtures.NOW); + // The same secret as a replaced entry is not a duplicate any more. + var report = AegisFixtures.runImport(vault, ImporterFixtures.uri("New", "c", AegisFixtures.SECRET_2), + Importer.MODE_REPLACE, false, ImporterFixtures.NOW); + var ids = vault.ids(); + var newest = vault.header(ids[1]); + return report.removed == 2 && report.imported == 1 && report.duplicates == 0 && ids.size() == 2 + && ids[0].equals(manualId) && newest != null && newest.name.equals("New"); +} + +// JSON cut in two parts: the first waits (sealed in storage), the second +// completes the import and clears the buffer. +(:test) +function importerJoinsMultiPartJson(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var store = vault.storage() as MemoryBackend; + var full = AegisFixtures.export(3, [AegisFixtures.totp("A", "a", AegisFixtures.SECRET_1), + AegisFixtures.totp("B", "b", AegisFixtures.SECRET_2), AegisFixtures.totp("C", "c", AegisFixtures.SECRET_3)], ""); + var cut = full.length() / 2; + var part1 = AegisFixtures.runImport(vault, full.substring(0, cut) as String, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var waiting = part1.waitingChars == cut && ImportBuffer.hasPending(store) && vault.ids().size() == 0 + && part1.summary().find("Waiting for more") != null; + // The buffer is sealed: the secret's Base32 text isn't readable in storage. + var chunk = store.raw("imp.0") as ByteArray; + var sealed = Bytes.toHex(chunk).find(Bytes.toHex(Bytes.fromUtf8(AegisFixtures.SECRET_1))) == null; + var part2 = AegisFixtures.runImport(vault, full.substring(cut, full.length()) as String, Importer.MODE_APPEND, false, + ImporterFixtures.NOW + 60); + return waiting && sealed && part2.imported == 3 && !ImportBuffer.hasPending(store); +} + +(:test) +function importerDiscardsExpiredParts(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var full = AegisFixtures.export(3, [AegisFixtures.totp("A", "a", AegisFixtures.SECRET_1)], ""); + var cut = full.length() / 2; + AegisFixtures.runImport(vault, full.substring(0, cut) as String, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var late = AegisFixtures.runImport(vault, full.substring(cut, full.length()) as String, Importer.MODE_APPEND, false, + ImporterFixtures.NOW + ImportBuffer.MAX_AGE_S + 1); + return late.failure != null && vault.ids().size() == 0 && !ImportBuffer.hasPending(vault.storage()); +} + +(:test) +function importerExplainsUnusableText(logger as Test.Logger) as Boolean { + var cases = [ + ["", "empty"], + ["hello world", "not recognised"], + ["otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8", "Google Authenticator"], + ["{\"version\": 1, \"header\": {\"slots\": [1]}, \"db\": \"abc\"}", "encrypted"], + ["{\"version\": 1, \"db\": [1,]}", "not valid JSON"] + ]; + for (var i = 0; i < cases.size(); i++) { + var c = cases[i] as Array<String>; + var report = AegisFixtures.runImport(ImporterFixtures.vault(), c[0], Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var failure = report.failure; + if (failure == null || failure.find(c[1]) == null) { + logger.error("case " + i + ": " + failure); + return false; + } + } + return true; +} + +// A store that fills up mid-import keeps the whole entries stored so far. +(:test) +function importerStopsCleanlyWhenTheStoreIsFull(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var store = vault.storage() as MemoryBackend; + vault.add(StorageFixtures.totp("Existing")); + store.capacity = store.usedBytes() + 500; + var text = ""; + for (var i = 0; i < 10; i++) { + text += ImporterFixtures.uri("E" + i, "a", Base32.encode(Bytes.fromUtf8("fake" + (200000 + i).toString()))) + " "; + } + var report = AegisFixtures.runImport(vault, text, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var reopened = new Vault(store, new FakeTime()); + logger.debug("full store: " + report.summary() + " " + report.details.toString()); + return report.imported >= 1 && report.imported < 10 && AegisFixtures.hasDetail(report, "Stopped") + && reopened.ids().size() == 1 + report.imported; +} + +// Fingerprints: same type and secret give the same 8 bytes, anything else +// differs; they are in the header and survive a round trip. +(:test) +function importerFingerprintsIdentifySecrets(logger as Test.Logger) as Boolean { + var vault = ImporterFixtures.vault(); + var a = StorageFixtures.totp("A"); + var b = StorageFixtures.totp("B"); + var c = StorageFixtures.hotp("C", 0l); + var d = StorageFixtures.totp("D"); + d.secret = Bytes.fromUtf8("another secret"); + var fa = vault.keyManager.fingerprint(a); + var id = vault.add(a); + var header = vault.header(id); + return fa.size() == 8 && Bytes.equal(fa, vault.keyManager.fingerprint(b)) + && !Bytes.equal(fa, vault.keyManager.fingerprint(c)) && !Bytes.equal(fa, vault.keyManager.fingerprint(d)) + && header != null && Bytes.equal(header.fingerprint as ByteArray, fa); +} + +// 100 entries from one Aegis export: all stored, timed for PERFORMANCE.md. +(:test) +function importerHandlesOneHundredEntries(logger as Test.Logger) as Boolean { + var entries = [] as Array<String>; + for (var i = 0; i < 100; i++) { + // Distinct fake secrets: Base32 of a counter-based 10-byte value. + var raw = Bytes.fromUtf8("fake" + (100000 + i).toString()); + entries.add(AegisFixtures.totp("Issuer " + i, "user" + i + "@example.com", Base32.encode(raw))); + } + var text = AegisFixtures.export(3, entries, ""); + var vault = ImporterFixtures.vault(); + var start = System.getTimer(); + var job = new Importer(vault, text, Importer.MODE_APPEND, false, ImporterFixtures.NOW); + var steps = 0; + while (!job.step(1)) { + steps++; + } + var elapsed = System.getTimer() - start; + logger.debug("BENCH import_100_aegis ms=" + elapsed + " steps=" + steps + " json_bytes=" + text.length()); + return job.report.imported == 100 && vault.ids().size() == 100; +} diff --git a/test/import/JsonTest.mc b/test/import/JsonTest.mc new file mode 100644 index 0000000..c98aaa4 --- /dev/null +++ b/test/import/JsonTest.mc @@ -0,0 +1,128 @@ +// Author: cirakg & claude ai +// Summary: Tests for the streaming JSON tokenizer: tokens, escapes, numbers, errors, incomplete input, limits. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +module JsonFixtures { + + // All tokens of text, as "kind:value" strings (values for keys, strings + // and numbers), ending with the final token. + function tokens(text as String) as Array<String> { + var tok = new Json.Tokenizer(Bytes.fromUtf8(text)); + var out = [] as Array<String>; + for (var i = 0; i < 200; i++) { + var t = tok.next(); + if (t == Json.T_KEY) { + out.add("K:" + tok.text); + } else if (t == Json.T_STRING) { + out.add("S:" + tok.text); + } else if (t == Json.T_NUMBER) { + out.add(tok.numberIsInteger ? "N:" + tok.number : "N:float"); + } else { + out.add("T" + t); + } + if (t == Json.T_END || t == Json.T_INCOMPLETE || t == Json.T_ERROR) { + return out; + } + } + return out; + } + + function last(text as String) as Number { + var tok = new Json.Tokenizer(Bytes.fromUtf8(text)); + for (var i = 0; i < 500; i++) { + var t = tok.next(); + if (t == Json.T_END || t == Json.T_INCOMPLETE || t == Json.T_ERROR) { + return t; + } + } + return -1; + } +} + +(:test) +function jsonTokenizesAllValueKinds(logger as Test.Logger) as Boolean { + var got = JsonFixtures.tokens("{\"a\": [1, -20, true, false, null, \"x\\\"y\\n\\u00e9\\ud83d\\ude00\"], \"b\": {}}"); + var expected = ["T1", "K:a", "T3", "N:1", "N:-20", "T8", "T9", "T10", "S:x\"y\né😀", "T4", "K:b", "T1", "T2", "T2", "T0"]; + if (!got.toString().equals(expected.toString())) { + logger.error("got " + got.toString()); + return false; + } + return true; +} + +(:test) +function jsonNumbers(logger as Test.Logger) as Boolean { + var got = JsonFixtures.tokens("[0, 1.5, 1e3, -0, 123456789012345678901, 999999999999999999]"); + var expected = ["T3", "N:0", "N:float", "N:float", "N:0", "N:float", "N:999999999999999999", "T4", "T0"]; + if (!got.toString().equals(expected.toString())) { + logger.error("got " + got.toString()); + return false; + } + return true; +} + +(:test) +function jsonRejectsInvalidDocuments(logger as Test.Logger) as Boolean { + var bad = [ + "[1,]", "{\"a\" 1}", "[01]", "[\"\\x\"]", "[\"\\ud800\"]", "[\"\\udc00\"]", "{a: 1}", "[1] [2]", + "[\"\\u0000\"]", "[1.]", "[-]", "[tru]", "{\"a\":1,}", "[\"a\tb\"]", "[1 2]", "]" + ]; + for (var i = 0; i < bad.size(); i++) { + if (JsonFixtures.last(bad[i] as String) != Json.T_ERROR) { + logger.error("accepted invalid JSON " + bad[i]); + return false; + } + } + return true; +} + +// Input that stops inside the document is "incomplete", not an error, so +// a multi-part import can wait for the rest. +(:test) +function jsonReportsIncompleteInput(logger as Test.Logger) as Boolean { + var partial = ["{", "{\"a\"", "{\"a\":", "{\"a\": \"abc", "{\"a\": 12", "[tr", "[1, {\"b\": [", "{\"a\": \"x\\"]; + for (var i = 0; i < partial.size(); i++) { + if (JsonFixtures.last(partial[i] as String) != Json.T_INCOMPLETE) { + logger.error("not incomplete: " + partial[i]); + return false; + } + } + return JsonFixtures.last("") == Json.T_INCOMPLETE && JsonFixtures.last(" 7 ") == Json.T_END; +} + +(:test) +function jsonEnforcesTheDepthLimit(logger as Test.Logger) as Boolean { + var ok = ""; + var tooDeep = ""; + for (var i = 0; i < Json.MAX_DEPTH; i++) { + ok += "["; + } + tooDeep = ok + "["; + var close = ""; + for (var i = 0; i < Json.MAX_DEPTH; i++) { + close += "]"; + } + return JsonFixtures.last(ok + close) == Json.T_END && JsonFixtures.last(tooDeep + close + "]") == Json.T_ERROR; +} + +// Long strings (icons) are skipped without decoding, including escaped +// quotes inside them; long keys are errors. +(:test) +function jsonSkipsLongStrings(logger as Test.Logger) as Boolean { + var big = ""; + for (var i = 0; i < 300; i++) { + big += "ABCDEFGHIJ"; + } + var withQuote = big + "\\\"" + big; + var got = JsonFixtures.tokens("{\"icon\": \"" + big + "\", \"q\": \"" + withQuote + "\", \"n\": \"ok\"}"); + var expected = ["T1", "K:icon", "T11", "K:q", "T11", "K:n", "S:ok", "T2", "T0"]; + if (!got.toString().equals(expected.toString())) { + logger.error("got " + got.toString()); + return false; + } + return JsonFixtures.last("{\"" + big + "\": 1}") == Json.T_ERROR + && JsonFixtures.last("{\"icon\": \"" + big) == Json.T_INCOMPLETE; +} diff --git a/test/import/OtpAuthTest.mc b/test/import/OtpAuthTest.mc new file mode 100644 index 0000000..67f43f5 --- /dev/null +++ b/test/import/OtpAuthTest.mc @@ -0,0 +1,99 @@ +// Author: cirakg & claude ai +// Summary: Tests for otpauth:// parsing: splitting, labels, parameters, Steam, separators and every validation error. + +import Toybox.Lang; +import Toybox.Test; + +(:test) +module OtpAuthFixtures { + + // RFC 4226 test key "12345678901234567890" in Base32. + const SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"; // lint: allow-literal (RFC test key) + + function parse(uri as String) as ImportItem { + return OtpAuth.parse(uri, 1); + } + + function entry(logger as Test.Logger, uri as String) as Entry? { + var item = parse(uri); + if (item.entry == null) { + logger.error("no entry for " + uri + ": " + item.problem + " / " + item.skipped); + } + return item.entry; + } +} + +(:test) +function otpauthSplitsRunTogetherUris(logger as Test.Logger) as Boolean { + var s = OtpAuthFixtures.SECRET; + var uris = OtpAuth.splitUris("junk otpauth://totp/A?secret=" + s + " \n OTPAUTH://totp/B?secret=" + s + + "otpauth://hotp/C?secret=" + s + "&counter=1\n"); + return uris.size() == 3 && (uris[0] as String).find("totp/A") != null && (uris[1] as String).find("totp/B") != null + && (uris[2] as String).find("counter=1") != null && (uris[2] as String).find("\n") == null; +} + +(:test) +function otpauthLabelsAndDefaults(logger as Test.Logger) as Boolean { + var e = OtpAuthFixtures.entry(logger, "otpauth://totp/ACME%20Co:john%40example.com?secret=" + OtpAuthFixtures.SECRET); + var bare = OtpAuthFixtures.entry(logger, "otpauth://totp/alice?secret=" + OtpAuthFixtures.SECRET.toLower()); + if (e == null || bare == null) { + return false; + } + return e.name.equals("ACME Co") && e.account.equals("john@example.com") && e.algorithm == Hashes.SHA1 + && e.digits == 6 && e.period == 30 && e.type == Entry.OTP_TOTP && e.source == Entry.SOURCE_IMPORT + && Bytes.equal(e.secret as ByteArray, Bytes.fromUtf8("12345678901234567890")) + && bare.name.equals("") && bare.account.equals("alice"); +} + +(:test) +function otpauthParametersAndSeparators(logger as Test.Logger) as Boolean { + var s = OtpAuthFixtures.SECRET; + var e = OtpAuthFixtures.entry(logger, + "otpauth://totp/Label:bob?SECRET=" + s + ";Algorithm=sha512;digits=8&period=60&image=https%3A%2F%2Fx&issuer=Big+Bank"); + var item = OtpAuthFixtures.parse("otpauth://totp/Old:bob?secret=" + s + "&issuer=New"); + if (e == null) { + return false; + } + return e.name.equals("Big Bank") && e.algorithm == Hashes.SHA512 && e.digits == 8 && e.period == 60 + && item.entry != null && (item.entry as Entry).name.equals("New") && item.note != null + && e.account.equals("bob"); +} + +(:test) +function otpauthHotpAndSteam(logger as Test.Logger) as Boolean { + var s = OtpAuthFixtures.SECRET; + var h = OtpAuthFixtures.entry(logger, "otpauth://hotp/VPN:dave?secret=" + s + "&counter=42"); + var noCounter = OtpAuthFixtures.parse("otpauth://hotp/VPN:dave?secret=" + s); + var steam = OtpAuthFixtures.entry(logger, "otpauth://steam/Steam:erin?secret=" + s); + var encoder = OtpAuthFixtures.entry(logger, "otpauth://totp/Steam:erin?secret=" + s + "&encoder=steam&digits=5"); + return h != null && h.type == Entry.OTP_HOTP && h.counter == 42l + && noCounter.problem != null && (noCounter.problem as String).find("counter") != null + && steam != null && steam.type == Entry.OTP_STEAM && encoder != null && encoder.type == Entry.OTP_STEAM; +} + +(:test) +function otpauthReportsEveryProblem(logger as Test.Logger) as Boolean { + var s = OtpAuthFixtures.SECRET; + var cases = [ + ["otpauth://totp/A:b", "secret missing"], + ["otpauth://totp/A:b?secret=" + s + "&digits=9", "9 digits"], + ["otpauth://totp/A:b?secret=" + s + "&digits=x", "not a whole number"], + ["otpauth://totp/A:b?secret=" + s + "&period=0", "period 0"], + ["otpauth://totp/A:b?secret=" + s + "&algorithm=MD5", "algorithm"], + ["otpauth://totp/A:b?secret=ABC1DEFG", "position 4"], + ["otpauth://totp/A%2:b?secret=" + s, "% escape"], + ["otpauth://totp/?secret=" + s, "issuer or an account"], + ["otpauth://totp", "no '/'"] + ]; + for (var i = 0; i < cases.size(); i++) { + var c = cases[i] as Array<String>; + var item = OtpAuthFixtures.parse(c[0]); + var problem = item.problem; + if (item.entry != null || problem == null || problem.find(c[1]) == null) { + logger.error("case " + i + ": problem " + problem); + return false; + } + } + var motp = OtpAuthFixtures.parse("otpauth://motp/A:b?secret=" + s); + return motp.skipped != null && motp.entry == null; +} diff --git a/tools/gen_fuzz_corpus.py b/tools/gen_fuzz_corpus.py new file mode 100644 index 0000000..87d11ae --- /dev/null +++ b/tools/gen_fuzz_corpus.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# Author: cirakg & claude ai +# Summary: Generate the seeded fuzz corpus for the import parsers: mutated Aegis JSON and otpauth URIs. +"""Seeded fuzz inputs for the Monkey C import tests (README 17.3). + +Starts from small valid documents (fake data only) and applies random +mutations: truncation, deletion, insertion of random or structural bytes, +byte flips, duplicated spans, deep nesting and long strings. The output is +deterministic for a given seed, so the corpus in git is reproducible: + + gen_fuzz_corpus.py [--seed 4242] [--out test/data/FuzzCorpus.mc] + +Inputs are written as hex, because many aren't valid UTF-8; the tests feed +the bytes to the tokenizer and the Aegis reader, and the valid-UTF-8 ones +to the whole importer as text. +""" + +from __future__ import annotations + +import argparse +import random +from pathlib import Path + +SECRET = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ" # RFC 4226 test key in Base32, not a real secret +AEGIS_SEED = ( + '{"version": 1, "header": {"slots": null, "params": null}, "db": {"version": 3, "entries": [' + '{"type": "totp", "uuid": "u1", "name": "alice", "issuer": "GitHub", "note": "", "favorite": true, ' + '"icon": null, "info": {"secret": "' + SECRET + '", "algo": "SHA1", "digits": 6, "period": 30}, ' + '"groups": ["g1"]}, ' + '{"type": "hotp", "name": "b\\u00e9b", "issuer": "VPN", "info": {"secret": "' + SECRET + '", ' + '"algo": "SHA256", "digits": 8, "period": 30, "counter": 7}}], ' + '"groups": [{"uuid": "g1", "name": "Work"}]}}' +) +URI_SEEDS = [ + "otpauth://totp/GitHub:alice%40example.com?secret=" + SECRET + "&issuer=GitHub&digits=6", + "otpauth://hotp/VPN:bob?secret=" + SECRET + "&counter=5;algorithm=SHA256", + "otpauth://steam/Steam:carol?secret=" + SECRET, +] +STRUCTURAL = [ + *(b"{", b"}", b"[", b"]", b'"', b":", b",", b"\\", b"\\u", b"\\ud83d"), + *(b"null", b"-", b"1e9", b"%", b"&", b"=", b"?"), +] + + +def mutate(data: bytes, rng: random.Random) -> bytes: + """One to three random mutations of data.""" + out = bytearray(data) + for _ in range(rng.randint(1, 3)): + choice = rng.randrange(7) + at = rng.randrange(len(out) + 1) if out else 0 + if choice == 0 and out: # truncate + del out[at:] + elif choice == 1 and out: # delete a span + del out[at : at + rng.randint(1, 8)] + elif choice == 2: # insert random bytes + out[at:at] = bytes(rng.randrange(256) for _ in range(rng.randint(1, 4))) + elif choice == 3: # insert a structural token + out[at:at] = rng.choice(STRUCTURAL) + elif choice == 4 and out: # flip a byte + pos = rng.randrange(len(out)) + out[pos] ^= 1 << rng.randrange(8) + elif choice == 5 and out: # duplicate a span + span = out[at : at + rng.randint(1, 30)] + out[at:at] = span + else: # deep nesting or a long string + long_string = b'"' + b"A" * rng.randint(520, 1100) + b'"' + out[at:at] = b"[" * rng.randint(10, 30) if rng.random() < 0.5 else long_string + return bytes(out) + + +def corpus(seed: int, count_json: int, count_uri: int) -> tuple[list[bytes], list[bytes]]: + rng = random.Random(seed) + base = AEGIS_SEED.encode() + jsons = [base] + [mutate(base, rng) for _ in range(count_json - 1)] + mutated = [mutate(rng.choice(URI_SEEDS).encode(), rng) for _ in range(count_uri - len(URI_SEEDS))] + uris = [u.encode() for u in URI_SEEDS] + mutated + return jsons, uris + + +def render(jsons: list[bytes], uris: list[bytes], seed: int) -> str: + def table(name: str, items: list[bytes]) -> str: + last = len(items) - 1 + rows = "\n".join( + f' "{item.hex()}"{"" if i == last else ","} // lint: allow-literal' for i, item in enumerate(items) + ) + return f" const {name} = [\n{rows}\n ];\n" + + return ( + "// Author: cirakg & claude ai\n" + "// Summary: Seeded fuzz inputs for the import parsers (generated by tools/gen_fuzz_corpus.py; do not edit).\n" + f"//\n// Seed {seed}. Hex because many inputs are not valid UTF-8. Fake data only.\n\n" + "import Toybox.Lang;\n\n(:test)\nmodule FuzzCorpus {\n" + + table("JSON", jsons) + + "\n" + + table("URIS", uris) + + "}\n" + ) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--seed", type=int, default=4242) + parser.add_argument("--out", type=Path, default=Path("test/data/FuzzCorpus.mc")) + parser.add_argument("--json", type=int, default=50) + parser.add_argument("--uris", type=int, default=50) + args = parser.parse_args(argv) + jsons, uris = corpus(args.seed, args.json, args.uris) + args.out.write_text(render(jsons, uris, args.seed), encoding="utf-8") + print(f"wrote {len(jsons)} JSON and {len(uris)} URI inputs to {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tests/test_gen_fuzz_corpus.py b/tools/tests/test_gen_fuzz_corpus.py new file mode 100644 index 0000000..5ce8c99 --- /dev/null +++ b/tools/tests/test_gen_fuzz_corpus.py @@ -0,0 +1,41 @@ +# Author: cirakg & claude ai +# Summary: Tests for the fuzz corpus generator: determinism, mutation, valid Monkey C output, and the committed corpus. +from __future__ import annotations + +import random +from pathlib import Path + +import gen_fuzz_corpus + +ROOT = Path(__file__).resolve().parents[2] + + +def test_same_seed_gives_the_same_corpus() -> None: + assert gen_fuzz_corpus.corpus(7, 10, 10) == gen_fuzz_corpus.corpus(7, 10, 10) + assert gen_fuzz_corpus.corpus(7, 10, 10) != gen_fuzz_corpus.corpus(8, 10, 10) + + +def test_corpus_starts_with_the_valid_seeds() -> None: + jsons, uris = gen_fuzz_corpus.corpus(1, 5, 5) + assert jsons[0] == gen_fuzz_corpus.AEGIS_SEED.encode() + assert uris[: len(gen_fuzz_corpus.URI_SEEDS)] == [u.encode() for u in gen_fuzz_corpus.URI_SEEDS] + assert len(jsons) == 5 and len(uris) == 5 + + +def test_mutate_changes_the_input() -> None: + rng = random.Random(3) + base = gen_fuzz_corpus.AEGIS_SEED.encode() + assert sum(gen_fuzz_corpus.mutate(base, rng) != base for _ in range(20)) >= 19 + + +def test_render_puts_commas_outside_the_comments() -> None: + text = gen_fuzz_corpus.render([b"{}", b"[]"], [b"x"], 1) + assert '"7b7d", // lint: allow-literal' in text + assert '"5b5d" // lint: allow-literal' in text + assert text.startswith("// Author: cirakg & claude ai\n// Summary:") + + +def test_committed_corpus_matches_the_generator() -> None: + jsons, uris = gen_fuzz_corpus.corpus(4242, 50, 50) + committed = (ROOT / "test" / "data" / "FuzzCorpus.mc").read_text(encoding="utf-8") + assert committed == gen_fuzz_corpus.render(jsons, uris, 4242) diff --git a/tools/ui_flow.py b/tools/ui_flow.py index f43cf81..3e82231 100644 --- a/tools/ui_flow.py +++ b/tools/ui_flow.py @@ -19,7 +19,8 @@ drawing each screen, so every step waits for the screen it expects instead of sleeping. Menus are native Menu2 views that print nothing; for those the script waits a fixed time. -The flow covers: the list, a TOTP entry, a HOTP entry and its next code, the +The flow covers: the results of an Aegis import sent through the settings +form (the fixture sets the form, as the phone would), the list, a TOTP entry, a HOTP entry and its next code, the PIN screen, a wrong PIN, a correct PIN opening a locked entry, auto-lock closing it, the menu, the About screen (watches with buttons; touch-only menus have no focus to move) and the lockout after 5 wrong PINs. @@ -46,6 +47,8 @@ import sim_screenshot as shots EXIT_OK, EXIT_DIFFERENT, EXIT_FLOW = 0, 1, 2 SCREEN_TIMEOUT_S = 20.0 AUTO_LOCK_TIMEOUT_S = 25.0 # the UI-test build locks after 10 s without input +IMPORT_TIMEOUT_S = 90.0 # the 100-entry fixture import runs on a timer after start-up +IMPORT_MEMORY_BUDGET = 0.70 # README 18: peak memory at most 70 % of the watch-app memory MENU_SETTLE_S = 1.5 # Menu2 has no "shown" line SETTLE_S = 0.4 # after a "shown" line, before the screenshot CLICK_PAUSE_S = 0.25 @@ -225,6 +228,12 @@ class Flow: else: self.sim.swipe(self._w() // 2, self._h() * 3 // 4, self._w() // 2, self._h() // 4) + def prev(self) -> None: + if self.sim.has("up"): + self.sim.press("up") + else: + self.sim.swipe(self._w() // 2, self._h() // 4, self._w() // 2, self._h() * 3 // 4) + def back(self) -> None: self.sim.press("esc") @@ -305,9 +314,26 @@ class Flow: self.results[name] = shots.check_golden(screen, label, out, golden) +def flow_import(f: Flow) -> None: + """The fixture's 100-entry Aegis import through the settings form opens its + results; its peak memory must stay within the budget.""" + f.expect("results", IMPORT_TIMEOUT_S) + stats = f.log.latest(r"DIAG import entries=(\d+) wall_ms=(\d+) mem_peak=(\d+) mem_total=(\d+)") + if stats is None: + raise FlowError("the import didn't report its statistics") + entries, wall_ms, peak, total = (int(v) for v in stats.groups()) + fraction = peak / total + print(f"[{f.sim.device_dir.name}] import of {entries} entries: {wall_ms} ms, peak memory {fraction:.1%}") + if fraction > IMPORT_MEMORY_BUDGET: + raise FlowError(f"import peak memory {fraction:.1%} is over {IMPORT_MEMORY_BUDGET:.0%}") + f.shot("import_results") + f.act() + f.back() + f.expect("list") + + def flow_entries(f: Flow) -> None: """The list, a TOTP entry, a HOTP entry and its next code.""" - f.expect("list") f.shot("list") f.act() f.open_focused_row() # GitHub, TOTP @@ -333,8 +359,7 @@ def flow_entries(f: Flow) -> None: def flow_locked_entry(f: Flow) -> None: """Bank is locked: PIN screen, a wrong PIN, the right PIN, then auto-lock.""" - for _ in range(6): # wrap around to Bank (index 2 of 7) - f.next() + f.prev() # from VPN back to Bank f.act() f.open_focused_row() f.expect("pin") @@ -391,6 +416,7 @@ def flow_lockout(f: Flow) -> None: def run_flow(f: Flow) -> None: """The scripted session; see the module docstring.""" + flow_import(f) flow_entries(f) flow_locked_entry(f) flow_menu(f) |
