24 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
25f32f7870 |
test(sms): build a real test suite (Phase G)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
nigig-map / test (push) Failing after 1s
sms / gates (push) Successful in 3s
sms / robius-sms (push) Successful in 23s
sms / android (push) Failing after 54s
sms / nigig-sms (push) Successful in 4m12s
sms / supply-chain (push) Successful in 6s
50 tests -> 102, and the two that were there at the start of this work
are deleted.
Where this started: robius-sms had ZERO tests, and nigig-sms had two --
bulk_sub_tab_default_is_contacts and bulk_sub_tab_variants_distinct.
Both asserted a derived Default and a derived PartialEq. Neither
mentioned SMS. Neither could fail short of the compiler breaking. That
is the defect that produced every other defect in this plan: nothing
could prove a change was safe, so nothing was ever deleted and every
bug survived contact with review.
Property tests (proptest, new dev-dependency)
Seven over truncate_preview, format_timestamp, badge_text, and five
more over segment_count, the rate limiter and ScheduleRequest.
These are the ones that matter, because the hand-written cases in this
repo all encode a bug someone had ALREADY found. proptest searches the
space instead. I verified that by reinstating the original byte-slicing
truncate_preview and confirming
prop_truncate_preview_survives_mixed_scripts and
prop_truncate_preview_respects_the_char_limit both fail against it --
they would have caught A3 before it shipped.
prop_rate_limiter_respects_capacity models the window independently
and asserts the invariant across random clock sequences, rather than
re-implementing the limiter's own arithmetic in the assertion.
Integration tests (2 new files, public API only)
robius-sms/tests/sms_pipeline.rs and nigig-core/tests/sms_store.rs go
through the public surface the application actually uses. The unit
tests inside src/ can see private helpers; these cannot, which is the
point -- they catch a refactor that keeps every unit test green while
breaking the caller-visible contract.
Two of them are privacy canaries. e1_message_bodies_are_never_persisted
and e1_no_body_text_reaches_the_serialised_store fail if anyone removes
#[serde(skip)] from OfflineSmsMessage.body. Verified by removing it:
both fail, the other six pass. Nothing else in the tree would have
noticed the inbox silently going back to plaintext on disk.
Named regression tests
One per defect, named for it -- c1_*, d1_*, d3_*, e1_*, e7_*, a4_*,
c3_*, c7_* -- so a future reader goes from a failing test straight to
the bug it guards rather than to a git archaeology session.
New coverage for logic that had none
- build_timeline_items / build_filtered_timeline_items: date-divider
placement and the message indices the draw loop uses to index
conv_data.messages. An off-by-one there renders the wrong body in
the wrong bubble; it had no test at all.
- kind_to_offline / kind_from_offline round-trip: the only thing
stopping a cached Sent message reappearing as Inbox after a restart,
which would flip the bubble to the wrong side of the screen.
- normalize_number: what C1 groups on, across five formatting variants
plus short codes and alphanumeric senders.
MessageKind::from_android_type / to_android_type were hoisted out of
sys/android/inbox.rs onto the type, the same way ScheduleRequest::validate
was in A4, so the provider mapping is testable off-device. An
unrecognised TYPE value is preserved verbatim in Unknown rather than
defaulted, and there is a property test asserting the round trip is
total over every i32.
CI: a test-count FLOOR at 100. A floor rather than a ratchet -- unlike
the clippy count, there is no reason to ever want this number to fall.
Deliberately NOT faked: the JNI cursor loop, the keystore round-trip and
broadcast delivery still need an emulator. A mock returning what I expect
would test my expectations, not Android. Those remain called out in the
Phase A and E commit messages.
Verified: 11/11 checks. 48 robius-sms + 46 nigig-sms + 8 sms_store = 102.
clippy -D warnings clean on host and aarch64-linux-android; nigig-sms
ratchet holds at 32 (my first draft added an orphaned `use super::*`,
caught by the ratchet and removed rather than baselined).
|
||
|
|
1670ddf49c |
refactor(sms): delete the dead code and the duplication (Phase F)
Some checks failed
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
Net -596 lines. No behaviour change except F10, which replaces a label that was lying. F2 -- four copies of one stub backend. apple.rs, linux.rs and windows.rs were BYTE-IDENTICAL 66-line files, and unsupported.rs was the same again. That duplication is what let them drift: Phase C3 had to fix `Error::Unknown` in exactly one of the four, because only one had it wrong. Collapsed into sys/stub.rs, which each platform module invokes. 268 lines become 35 plus one shared definition. The module is cfg'd out on Android, which has a real implementation and would otherwise report the macro as unused under -D warnings. F3 -- TWO dead compose implementations. SmsComposePage (189 lines) was registered in the VM and instantiated nowhere. Separately, the FAB and its compose overlay were left in the DSL as `visible: false` with a comment saying "FAB removed: SMS compose/inbox navigation now lives in SmsActionBar" -- but 102 lines of DSL and 53 lines of handler stayed behind, wired to a button no user can reach. Deleted both, and send_reply() with them: it existed only to serve the unreachable overlay. Compose navigation is SmsActionBar's, as the comment already said. F4 -- a whole second contact subsystem, unreachable. sms_screen.rs carried its own CONTACTS_CACHE, contacts_loaded(), load_contacts_into_cache(), display_name_for_number(), normalize_number(), try_load_contacts() and a contacts_load_attempted field. Nothing called any of it -- the live implementation is in conversations_list.rs. Worth noting the dead copy was also the WRONG one: its display_name_for_number did an O(n) linear scan of the whole phone book per lookup, where the live version is O(1) because cache_contact_number inserts under both the raw and normalised key. F5 -- the page tree was written out twice. sms_bulk_page, sms_schedule_page and sms_more_page were each declared under Desktop AND under Mobile, byte-identical apart from indentation. Any change to a page header had to be made in both places or the layouts silently diverged. Now three named widgets plus a shared SmsPageHeader, referenced from both variants. F8 -- serde, serde_json and robius-location were declared by nigig-sms and referenced nowhere in its sources. F9 -- was_scrolling was read twice per frame from the same portal list; the copy in handle_event was bound and never used. F10 -- the character counter was a hardcoded lie. The old compose page rendered "0 / 160 characters" and never updated it. It died with F3, but the bulk composer -- where the money actually goes -- had no cost indication at all. It now shows live segment count as you type, using segment_count() from Phase A5, because segments are the billing unit and "160" is only right for GSM-7: one emoji forces UCS-2 and drops the limit to 70. This is the only user-visible change in the commit. F1 and F7 were already done, in Phase A (shared cursor.rs) and Phase D1 (I/O out of draw_walk). The deletions orphaned eight imports, which are also removed. Together that takes the nigig-sms clippy ratchet from 49 to 32 -- these were not suppressed, the code they reported on is gone. Verified: 10/10 checks. clippy -D warnings clean on host AND aarch64-linux-android, 28 robius-sms tests, 22 nigig-sms tests, nigig-build still builds, metadata --locked clean. |
||
|
|
cbe47a5f6c |
perf(sms): close the outstanding Phase B and D items
Some checks failed
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
An audit of Phase 0 through D against the tree found three tasks marked
done in prose but absent from the code. This closes them.
D5 -- bulk send still blocked the UI thread.
The send loop ran synchronously in handle_send_bulk.
SmsManager.sendTextMessage queues to the radio and rate-limits, so a
200-recipient batch was a multi-minute ANR with no progress and no
way to tell whether anything was happening. Phase E8's throttle
bounded the worst case at 30 sends, but bounded blocking is still
blocking -- and I said as much when deferring it.
Now uses the worker pattern D1 established: spawn, publish progress
through a mutex-guarded slot, SignalToUI, drain on the UI thread. The
status line counts up ("Sending 12/200…") instead of freezing.
BULK_SEND_IN_FLIGHT prevents two overlapping batches.
D2 -- the ContentObserver, the half I left open.
Phase D removed the 5-second poll that re-armed itself via redraw()
and stopped the app ever idling. That fixed the busy loop but left a
gap I documented rather than closed: a message arriving while the app
was open did not surface until the next Resume or manual pull.
Adds SmsInboxObserver.java -- a ContentObserver on content://sms,
registered with a main-Looper Handler, idempotent so onResume can call
it freely -- compiled and dexed by the existing build.rs pipeline and
loaded through the same in-memory dex loader as the receivers.
onChange calls into Rust, which does two cheap things: set an atomic,
and invoke a registered waker. The waker matters. robius-sms has no UI
dependency and cannot call SignalToUI itself, so without it the flag
would only be observed on the next event-loop turn that happened for
some other reason -- which, with the poll gone, might be never while
the app sits idle. The app registers SignalToUI::set_ui_signal, so
this is a genuine push.
Native binding is dynamic, not #[no_mangle], for the same reason as
E6: the class comes from an in-memory dex and is not on the JVM's
search path.
B0 (wider) -- 23 crates declared robius-sms and never called it.
Phase B removed the three declarations that put RUSTSEC advisories on
nigig-build and explicitly flagged the rest as "the same latent
problem, sweep separately". This is that sweep: every crate with zero
references to robius_sms in its sources loses the dependency.
nigig-mpesa, nigig-pay and nigig-sms keep it -- they are the only real
users. nigig-system-prefs only mentions robius-sms in its package
description, so its manifest is untouched.
Cargo.lock loses another 21 lines.
Verified: 13/13 CI checks. clippy -D warnings clean on host and
aarch64-linux-android; the Android build compiles, javac-builds and
dexes the new observer class. cargo deny still "advisories ok, bans ok,
licenses ok, sources ok". clippy ratchet holds at 49. Sampled four of
the 23 stripped crates plus all five I edited; all build.
Pre-existing and unrelated: nigig-map fails to compile on pristine
origin/main (12 errors in view.rs, a Script/Widget derive problem), so
pageflipnav and anything else reaching it cannot be checked here. I
touched no files under crates/apps/map.
NOT verified on a device. The observer's registration, the onChange
callback and the waker all need an emulator or handset with a live SMS
provider; this sandbox has neither, and there is still no CI runner.
|
||
|
|
a38c41c00a |
security(sms): stop persisting message bodies, throttle sends (Phase E)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
E1 -- the inbox was written to disk in plaintext. offline_store wrote every SMS body to app_data_dir/offline_store/sms_messages.json as pretty-printed JSON. SMS is the transport for OTPs, banking codes and M-Pesa confirmations, so that file was the user's complete authentication history sitting in app-private storage -- readable by anything running as the same UID, and included in backups. This repository already knew the answer. THREAT_MODEL.md T-I2 records "Raw SMS persisted to PSV file" as fixed in Phase 0, with raw_message omitted from save_to_disk() so it "lives in memory only". The SMS app then re-introduced the same defect at larger scale: the entire inbox rather than just M-Pesa messages, and with no retention limit until D6. OfflineSmsMessage.body is now #[serde(skip)]. Dropping the field rather than encrypting it is the deliberate choice: every consumer already reads the device provider FIRST and writes the cache second (nigig-sms fetch_from_device, and the mpesa and pay transaction pages), so the provider is the system of record and no body needs to survive a restart. Encryption would keep the plaintext reachable to anything holding the key. Not writing it removes the asset. Two consequences handled: sms_key() no longer hashes the body, since a reloaded row has an empty one and dedupe would otherwise never match its own cached entry and grow a duplicate per refresh; and the cached first paint shows a neutral placeholder rather than a blank preview for the instant before the provider read lands. E9 -- the Linux backend's dependencies were pure cost. robius-sms declared polkit =0.17.0 and gio =0.17.0 for target_os = "linux". sys/linux.rs references neither: all twelve functions return Err(PermanentlyUnavailable). Those two crates dragged in glib and proc-macro-error and were the origin of RUSTSEC-2024-0370 and RUSTSEC-2024-0429 for every consumer of this crate. Deleting the block removes 340 lines from Cargo.lock. polkit, gio, glib and proc-macro-error no longer appear in the workspace at all, which also closes the LGPL-2.1 linkage question outright rather than routing around it as Phase B did for nigig-build alone. E4 -- ROBIUS_SMS_BOOT_LIB was a code-injection vector. build.rs interpolated that environment variable straight into a Java string literal, which is then compiled, dexed and loaded at runtime with the app's full permissions. A value containing a quote closes the literal and injects arbitrary Java that runs on the device at boot. Build-time environment is not trusted input. Now validated against [A-Za-z0-9_]+ and the build fails loudly otherwise. Tested both ways: an exec payload is rejected, a legitimate name builds. E5 -- undefined behaviour in the dex loader. new_direct_byte_buffer was handed RECEIVER_BYTECODE.as_ptr() as *mut u8 -- a &'static [u8] in .rodata cast to a mutable pointer, when the API is documented as taking writable memory and InMemoryDexClassLoader may write through it. Now copies into an owned allocation and leaks it, which is correct rather than lazy: the buffer backs a ClassLoader cached in a OnceLock for the process lifetime. E6 -- two bindings for one native method. rustRestoreSchedules was both exported #[no_mangle] and registered dynamically via register_native_methods. Which one won was unspecified. Kept the dynamic one, because the class is loaded from an in-memory dex and is not on the JVM's search path, so symbol binding is not guaranteed to find it. E8 -- no send rate limiting. Nothing capped send rate, and the bulk UI exists to blast a scraped directory. Android's practical throttle is ~30 messages per 30 minutes per app, past which sends are silently dropped -- so an unthrottled batch both overspends and fails opaquely. Adds SendRateLimiter, a pure token bucket taking an explicit clock so it is unit-testable without sleeping, wired into the bulk sender. A 200-recipient blast now stops at 30 and says why. E10 -- robius-sms carried no license field, so cargo-deny needed a [[licenses.clarify]] override asserting one. Stated in the manifest; override removed. E2 was already satisfied by D6 (retention capped at 5,000). E7/E11 are documented rather than fixed, which is the honest status: delivery confirmation needs real PendingIntents plumbed through (A8 documents that Ok != delivered), and sender validation cannot be solved client-side. Both are now rows in THREAT_MODEL.md instead of findings in a markdown report -- along with T-I2b for E1 and T-I4 for E3, which is NOT done: scheduled message bodies are still plaintext in SharedPreferences. CI: adds a gate asserting OfflineSmsMessage.body keeps #[serde(skip)]. Removing that attribute silently resumes writing plaintext and nothing else would fail. Negative-tested. deny-nigig-build.toml drops to 2 exemptions (from 5 before Phase B). Tests: robius-sms 21 -> 25. Verified: 12/12 CI jobs, clippy -D warnings clean on host and aarch64-linux-android, cargo deny "advisories ok, bans ok, licenses ok, sources ok", clippy ratchet holds at 49. |
||
|
|
69dd169ca6 |
perf(sms): get blocking I/O off the render thread (Phase D)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
D1 -- the SMS read blocked the render thread. fetch_from_device() called robius_sms::list_messages() directly from draw_walk. That is a blocking cross-process ContentProvider query over Binder, plus ~10 JNI calls per message row, plus a full rewrite of the offline JSON store. On a populated inbox it is a multi-hundred- millisecond stall, taken on the render thread. robius_android_env::with_activity() calls attach_current_thread_permanently() and ContentResolver.query is thread-safe, so the read is safe off the UI thread. It now runs on a worker and hands back through a results queue drained on Event::Signal -- the same shape the contact lookup in this file already used. D2 -- the app never idled. draw_walk ran a 5-second wall-clock refresh, and that refresh called redraw(), which scheduled the next frame, which re-entered draw_walk. A self-sustaining loop, running whether or not the SMS tab was even visible, each cycle paying D1's cost plus D3's clone. Refresh is now event-driven: first load, Event::Resume, pull-to-refresh, and the permission-granted callback. NOT yet a ContentObserver on content://sms -- that is the remaining half of D2 and is called out in the code. Until it lands, a message arriving while the app is open appears on the next Resume or pull rather than within 5s. That is a deliberate trade: a bounded staleness window in exchange for an app that can reach idle. D3 -- a deep clone per refresh, purely to diff. `let previous = self.conversations.clone()` copied every message in every conversation on each cycle so the result could be compared for equality. Replaced with a u64 digest over (count, address, message_count, newest date_ms). Bodies are immutable once stored, so that is sufficient to notice an insert, a delete or a new message -- and the test asserts exactly those three cases. D4 -- ~900 pointless worker kicks per second. start_contact_lookup_worker() was called once per visible row per frame (~10-15 per frame, ~900/s at 60fps). Each call took 3-4 mutex locks before early-returning, contending with the worker thread trying to write results back. Now called once, after the draw pass. The per-row code only enqueues. D6 -- the offline store grew without bound. upsert_sms_messages re-read, re-hashed, re-sorted and rewrote the whole file on every call, with no cap -- while append_location() a few lines below has always truncated to 512. Capped at MAX_CACHED_SMS (5000), newest-first so truncate drops the oldest. This is a display cache; the device provider stays the system of record. D7 -- O(rows x selected) per frame in the bulk list. get_selected_companies() returns a Vec and the draw path called `selected.contains(..)` once per visible row, so "Select All" over the Nairobi directory made every frame quadratic. Now a HashSet. Same fix in sync_selected_to_recipients, whose phone de-duplication was also O(n^2) via `phones.contains()`. Refactor note: group_into_conversations() and conversations_digest_of() were lifted out as free functions. ConversationsList holds a Makepad View and is not Default, so nothing in it could be unit tested; the logic D1/D3 depend on now can be. CI: adds a gate rejecting blocking robius_sms provider calls inside any draw_walk. Negative-tested by reinserting the call and confirming it fails. Tests: nigig-sms 19 -> 22, nigig-core +1. Verified: clippy ratchet holds at 49, clippy -D warnings clean on host and aarch64-linux-android, nigig-build still builds. NOT measured. The plan asked for before/after frame timings on a 5,000 message fixture and this sandbox has no device or emulator, so the figures above are reasoned from the code, not profiled. D5 (bulk send still blocks the UI thread) is untouched and needs the same worker treatment as D1. Pre-existing and unrelated: nigig-core's pending_tx_lifecycle test fails identically on pristine origin/main (wall-clock assumption in an M-Pesa expiry test). |
||
|
|
0614a70888 |
fix(sms): correctness pass on grouping, errors, dates and iOS (Phase C)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
C1 -- one contact appeared as several conversations.
populate_display_messages() keyed the group HashMap on the RAW
provider address. A Kenyan inbox routinely carries three forms of the
same person -- +254712345678, 0712345678, 254712345678 -- depending on
whether they were on-net, roaming, or saved in contacts. Each became a
separate thread holding part of the history, and a reply went to
whichever one happened to be open, so the user's own messages
scattered across the duplicates.
normalize_number() (last 9 digits) already existed and was already
used for contact-NAME lookup; it just was not used for grouping.
get_conversation() and insert_sent_message() now match the same way,
so a reply joins the existing thread. Alphanumeric senders
("Safaricom", "MPESA") normalise to empty and fall back to the raw
address, so they are not all merged into one bucket.
C3 -- every JNI failure said "Unknown error".
From<jni::errors::Error> mapped everything to Error::Unknown,
discarding the payload. A failed send read identically whether the
cause was a missing method, a mismatched descriptor, a Java exception
or an unreachable JVM. Field diagnosis was impossible -- and this is
how the setRepeating signature bug (fixed in A4) stayed invisible.
Adds Error::Jni { kind: JniFailure, detail } and classifies. Also
fixes sys/unsupported.rs, which returned Unknown for all twelve
functions where every other stub backend returns
PermanentlyUnavailable, so unsupported targets reported "Unknown
error" instead of "not supported here".
C4 -- dead branch in show_conversation().
Two blocks; the second ran unconditionally and re-applied the
no-search behaviour, making the search branch above it dead. Opening a
conversation with an active filter scrolled to the bottom of the
UNFILTERED timeline instead of the top of the matches. A merge
artifact, invisible unless you read the control flow rather than the
surrounding "Mirror Robrix" comments.
C5 -- timestamps were wrong outside East Africa.
~160 lines of hand-rolled civil-date arithmetic with a hardcoded
UTC+3. The desktop branch attempted to read $TZ but stripped the
alphabetic characters and parsed the remainder, so "America/New_York"
became "/New_York", failed, and fell through to +3 as well: it could
never have worked for any named zone.
Deleted in favour of chrono, which was ALREADY a dependency of this
crate and already used correctly in conversation_screen.rs. Verified
green under TZ=UTC, Africa/Nairobi, America/New_York and Asia/Tokyo.
C6 -- iOS thread ids were from the wrong namespace.
read_modern_db selected m.handle_id -- a PARTICIPANT id -- and wrote
it into SmsMessage.thread_id. But list_thread_messages() filters on
chat_message_join.chat_id and read_modern_threads() reports
chat.ROWID. Three namespaces, so an id handed out on read never
matched the id expected on query: thread navigation on iOS was broken
by construction. Now joins chat_message_join and selects the chat id.
apple_date_to_ms() also assumed nanoseconds unconditionally; older
chat.db revisions store whole seconds in some columns, which divided
by 10^9 and rendered as 1970. Now disambiguates by magnitude.
C7 -- the bulk path skipped its validation.
The app never called send_bulk_sms; it wrote its own loop over
send_sms so it could report per-recipient success. That also skipped
validate_bulk_send_request, the only check that rejects a
whitespace-only recipient inside a batch -- a stray blank line in the
recipients box. Rather than give up the per-recipient reporting, the
rule moves onto BulkSendRequest::validate() and the caller runs it
explicitly.
C2 was already fixed in Phase A (A10, persisted id counter).
Tests: robius-sms 15 -> 21, nigig-sms 15 -> 19.
CI: nigig-sms clippy ratchet 50 -> 49 (C5 removed the dead helpers).
Verified: clippy -D warnings clean on host and aarch64-linux-android,
nigig-build still builds, cargo deny still passes.
|
||
|
|
0ed64c0435 |
fix(sms): send long messages whole, and confirm before bulk (A5, A7, A8)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
A5 -- long messages were silently truncated.
send_text_message() called SmsManager.sendTextMessage
unconditionally. That API is only defined for a body that fits one
PDU: 160 GSM-7 characters, or 70 once anything forces UCS-2. Past
that the behaviour is carrier- and OEM-dependent -- silent
truncation, silent failure, or an exception -- and the UI reported
"Sent" regardless, because a JNI call returning cleanly only means
the call was made.
Now uses divideMessage() + sendMultipartTextMessage() when the body
needs more than one part, so the recipient's handset reassembles it.
Adds segment_count(), which computes encoding and segment count on
any host so it is unit-testable and usable by UI. Correct GSM-7
modelling matters here and is not intuitive:
- the extended characters ^ { } \ [ ] ~ | € cost TWO septets each
- concatenation costs 7 septets per part, so the limit drops from
160 to 153 (and 70 to 67 for UCS-2)
- non-BMP characters such as emoji are surrogate pairs and cost two
UTF-16 units
- e/a/o/n/u with diacritics ARE in GSM-7. My first version of the
boundary test assumed é forced UCS-2 and failed; Swahili, French
and German text still bills at the 160-char rate. Arabic and
emoji do not.
A7 -- bulk send had no confirmation.
One tap on "Send SMS to Selected", one tap on Send, and N real,
billed, irreversible messages went out. For a feature whose purpose
is mass-messaging a scraped business directory, that is a
financial-harm defect rather than a UX gap.
Send now arms a confirmation quoting SEGMENTS, not messages, because
segments are what the user pays for and the number is surprising: one
emoji in the body forces UCS-2 and can turn "200 messages" into 800
paid segments. Editing the body or the recipient list re-arms the
prompt, so a confirmation cannot be inherited by different content.
A8 -- SCHEDULE_EXACT_ALARM was neither requested nor documented.
Android 12+ refuses exact alarms without it, and it is a
special-access permission: a runtime prompt cannot grant it, the user
must enable "Alarms & reminders" in Settings. Without it scheduled
sends are subject to Doze batching. Documented in the crate docs and
README, along with two things that were not written down anywhere:
Ok from send_sms means "handed to the platform", NOT delivered (both
PendingIntents are null, so nothing can report back); and callers are
billed per segment.
Verified: robius-sms 15 tests pass (8 new for segmentation), nigig-sms
15 pass, clippy -D warnings clean on host and aarch64-linux-android,
nigig-sms clippy ratchet holds at 50, gates and supply-chain green.
|
||
|
|
d96f8301ef |
fix(sms): make scheduling actually work (A4, A9, A10)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
Scheduled SMS could never have sent a single message. Four independent defects in one code path, all in code with no tests. A4 -- every request was rejected before reaching the platform. `ScheduleRequest.interval_ms` was a bare i64 and the validator required `interval_ms > 0`, but "send once" is the only mode the UI offers, so sms_schedule_page.rs passed `interval_ms: 0`. Result: 100% of scheduled messages failed with InvalidInput. The user saw "Scheduled: 0, Failed: N" and no reason. interval_ms is now Option<i64>: None = send once, Some(n>0) = repeat. The validator moves onto ScheduleRequest::validate() so it can be tested on any host -- the old copy was behind #[cfg(target_os = "android")], which is part of why a defect this total went unnoticed. Wrong JNI signature -- found while fixing the above, not in the original audit. setRepeating was invoked with the descriptor "(IJLandroid/app/PendingIntent;)V": three parameters, four arguments. AlarmManager.setRepeating is (int, long, long, PendingIntent), i.e. "(IJJLandroid/app/PendingIntent;)V". Even had A4 not rejected everything first, this would have thrown NoSuchMethodError at the JNI boundary -- and with no exception checking (A2, still open) that is a process abort, not an Err. A9 -- alarms did not wake the device. The type argument was hardcoded to 0 (AlarmManager.RTC). RTC does not fire while the device sleeps, so a 03:00 schedule waited until the user next picked up the phone. Now RTC_WAKEUP (1), as a named constant. A10 -- batches silently destroyed each other. Ids were `1000 + index`, so a second batch reused 1000..1000+n. PendingIntent.getBroadcast matches on request code and the crate passes FLAG_UPDATE_CURRENT, so the new alarm replaced the old one; store_schedule keys SharedPreferences on the same id, so the stored recipient and body went too. next_schedule_id() now allocates from a counter persisted under app_data_dir, because the collision has to be avoided across restarts, not just within a run. Persistence: SharedPreferences has no null long, so None is stored as ONE_SHOT_SENTINEL (0) and mapped back on load. Anything <= 0 reads back as None rather than being trusted as an interval -- a 0 interval would make setRepeating fire in a tight loop. Tests: robius-sms had NO tests. It now has 7, covering the validator's whole contract. accepts_the_one_shot_request_the_app_actually_builds constructs the exact shape sms_schedule_page.rs sends and fails against the old validator. Verified: robius-sms 7 tests pass, nigig-sms 15 pass, clippy -D warnings clean on host AND aarch64-linux-android, gates and supply-chain green. Still open on this path: A2 (no JNI exception checking) and SCHEDULE_EXACT_ALARM is neither requested nor documented, so exact delivery is not guaranteed on Android 12+. |
||
|
|
5cfeec26ff |
fix(sms): recover from mutex poisoning instead of panicking (A6)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
Every access to the SMS contact-cache statics used `.lock().unwrap()` -- 25 call sites across four files, 21 of them in conversations_list.rs. `Mutex::lock()` returns Err only when a previous holder panicked while the guard was alive. `.unwrap()` on that converts one transient panic into a permanent one: from then on EVERY later access to the same mutex panics. The cache is written from a spawned worker thread (start_contact_lookup_worker) and read from draw_walk, so a single panic in the worker left contact resolution broken for the rest of the process, surfacing as a panic in the renderer with no connection to the original fault. For this data, aborting is the wrong trade. The protected values are a name cache, a lookup queue and an in-flight flag. None has an invariant that a mid-update panic could break in a way that makes the data unsafe to read; the worst case is a half-populated cache, which self-corrects on the next lookup. Adds sms_frame/lock_ext.rs with LockRecover::lock_recover(), which recovers the guard via PoisonError::into_inner(). All 25 sites now use it. Note this crate already knew the answer and applied it inconsistently: companies_list.rs used `if let Ok(mut s) = ..lock()` in two places, which is poison-safe but silently DROPS the write. lock_recover() keeps the update. Tests: three, including stays_usable_across_repeated_access_once_poisoned, which poisons a mutex from a panicking thread and then performs 100 further accesses -- the exact failure mode described above, and one the old code could not survive past the first. CI: the .lock().unwrap() step goes from a ratchet at 19 to a HARD gate, since production code is now at zero. lock_ext.rs is excluded: its tests must poison a mutex to observe the Err, which needs a real .lock().unwrap(). Verified: 15 tests pass (was 12), clippy 50/50, hard gate passes. |
||
|
|
00290ad682 |
fix(sms): stop truncate_preview panicking on multi-byte text (A3)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
truncate_preview() compared `trimmed.len()` -- BYTES -- against
PREVIEW_MAX_CHARS, then sliced the string at that byte offset:
if trimmed.len() <= PREVIEW_MAX_CHARS { .. }
else {
let mut end = PREVIEW_MAX_CHARS; // 120, bytes
if let Some(pos) = trimmed[..end].rfind(..) { .. } // panics
format!("{}…", &trimmed[..end]) // panics
}
Slicing a &str at a byte offset that is not a character boundary
panics. This function runs inside draw_walk, once per visible row per
frame, over message bodies that arrive from anyone who knows the
device's number -- so a single such message took down the conversation
list on every frame until it was deleted.
Why it survived review, and why my own first repro was wrong: it does
NOT fire on uniform multi-byte text. 120 is divisible by 2, 3 and 4, so
a body of pure emoji or pure Arabic happens to land exactly on a
boundary. I initially "confirmed" the bug with 40 emoji and got a clean
pass. It needs MIXED text -- any misaligning run of ASCII before the
multi-byte part -- which is the normal shape of real traffic:
"a" + 40 emoji panicked
"Hi " + 40 emoji panicked
"Confirmed. Ksh1,000 sent to JOHN DOE ..." + 🎂 panicked
"x" + 130 é panicked
The fix counts characters via char_indices().nth(), and every offset it
slices at now comes from char_indices() or rfind() on a &str, both of
which return character-start offsets by construction.
Two behaviour changes fall out of counting correctly, both fixes:
- Bodies under 120 CHARACTERS are no longer truncated. Three of the
four cases above are 41-78 chars; they were being cut only because
120 bytes of emoji is 30 characters. A real M-Pesa confirmation
with a trailing emoji now displays in full.
- A long first word no longer collapses to a bare "…": the
whitespace break is only honoured when it leaves something to show.
Tests: 10 new unit tests, including never_panics_for_any_prefix_alignment,
which sweeps a 0-7 char ASCII prefix across 2-, 3- and 4-byte fillers so
the cut offset lands at every possible position inside a character. That
sweep fails against the old implementation.
Lints: sms_utils.rs moves from #![warn] to #![deny] for
clippy::indexing_slicing and clippy::string_slice. The two remaining
slices carry a scoped #[allow] with the boundary proof written out --
clippy cannot distinguish a proven-safe offset from an arbitrary one, so
the exemption is explicit and argued rather than a blanket mute.
CI: the byte-offset gate stays pinned at 2 (those two proven-safe
sites) with a comment recording that the real guarantee is now the deny
plus the tests, not the grep. The clippy ratchet returns 52 -> 50, since
the two string_slice reports Phase 0.7 surfaced are gone.
Verified: 12 tests pass (was 2), clippy 50/50, gates 19/19 and 2/2,
metadata --locked clean.
|
||
|
|
486fa5f168 |
ci(sms): finish Phase 0.7 and close a false negative in the slice gate
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
Two defects in my own Phase 0 work, both found by re-checking the
plan's task list against what actually shipped.
1. Task 0.7 was never done. The previous commit's subject line claimed
"Phase 0.2-0.7" but no lint attribute was ever added. sms_utils.rs
now carries module-level
#![warn(clippy::indexing_slicing)]
#![warn(clippy::string_slice)]
This module formats untrusted text -- SMS bodies straight off the
wire -- and every function in it runs inside draw_walk, once per
visible row per frame.
warn rather than deny: the two known sites are still present and
deny would make the crate fail to build. The point is that a THIRD
site cannot appear silently. Raise to deny when A3 lands.
2. The byte-offset slice gate had a false negative. It required a
leading `&`:
&[A-Za-z_][A-Za-z0-9_]*\[\.\.[A-Za-z0-9_]+\]
truncate_preview() contains two slices one line apart:
193: if let Some(pos) = trimmed[..end].rfind(..) <- MISSED
196: format!("{}…", &trimmed[..end]) <- caught
Line 193 is autoref'd, panics identically, and being first is the
one that actually fires. The gate reported "1" and looked audited
while missing the instance that runs. Pattern no longer requires the
`&` and also covers `[n..]` and `[..=n]`; baseline 1 -> 2.
The clippy ratchet moves 50 -> 52: the two new diagnostics are exactly
the clippy::string_slice reports for those two lines, which is the
intended effect of 0.7 -- A3 is now visible in CI instead of only in a
markdown file.
Verified with the pinned toolchain (1.97.1):
gates: lock().unwrap() 19/19 PASS, byte-slice 2/2 PASS
robius-sms: clippy -D warnings PASS, test PASS
nigig-sms: check PASS, test 2 passed, clippy ratchet 52/52 PASS
supply-chain: metadata --locked PASS, lock unchanged, whitespace PASS
repo-hygiene: markers PASS, workflow YAML PASS, 40-char SHA PASS
|
||
|
|
5e714577fb |
ci: require full 40-character SHAs for git dependency revs
Some checks failed
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Upstream adopted the makepad fork fix (
|
||
| 8fdff3ff55 |
Update makepad fork to a79f0dc (remove duplicate dependencies)
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
repo hygiene / hygiene (push) Has been cancelled
nigig-map / test (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
Commit a79f0dc fixes the duplicate dependency declarations that were causing TOML parsing errors. This is the correct commit to use after the parallel fixes in 5eda8056 and 11375214. All 34 Cargo.toml files updated to reference the correct commit. |
|||
|
|
80425bbfb8 |
fix: repair the makepad fork and unblock the build
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
The repository has been uncompilable since the d6d1f99c fork bump. Root cause was in gitdab.com/andodeki/makepad, not here, so the fix went there first: commit 5eda8056 on portallist_flow_adaptive_view. TWO defects, both introduced by the "Update fork to upstream dev 5d4483f" merge, both pure losses rather than intentional changes: 1. widgets/Cargo.toml: the makepad-gltf / makepad-csg / makepad-test dependency lines were relocated from [dependencies] to below [features]. Cargo then parses each as a feature whose value should be an array, giving "invalid type: map, expected a sequence", and the gltf/csg/test/maps features cease to exist. 2. widgets/src/lib.rs: the feature-gated re-export block for those same crates (plus makepad_fast_inflate and makepad_mbtile_reader) was deleted outright. Fixing only the manifest surfaced this as "no `makepad_csg` in the root". Both restored verbatim from 2c5cd97, the last rev that resolved. Neither is a judgement call: the moved lines are byte-identical and the deleted block is copied back unchanged. This repo is then repinned from d6d1f99c to the fixed rev, full 40-char SHA per the pinning convention CI enforces. Verified end to end after removing the local git redirect used during development, so this resolves against the real remote: cargo metadata resolves nigig-build --lib 685 passed cad_integration 154 passed spreadsheet-engine 225 passed doc-engine 53 passed nigig-map (maps feature) compiles Cargo.lock unchanged, --locked passes Also resolved committed conflict markers in two workflow files, which had made nigig-build.yml invalid YAML -- the CI config could not be parsed at all: - nigig-build.yml: kept --include='*.rs' on the by-value-getter gate. Without it the gate scans ARCHITECTURE.md and fails on its own documentation, which is the bug fixed in 4f32b1c. - pdf.yml: kept upstream's side. Enumerating targets via `cargo fuzz list` and failing when the list is empty is strictly better than a hardcoded target list that silently passes vacuously if a target is renamed. That makes four files in three commits now carrying committed conflict markers from this merge. Worth checking how they are reaching main -- `git diff --check` catches exactly this and is already a step in the nigig-build workflow, but it only runs on paths under that workflow's filter. |
||
| 4eebae14f2 |
Update makepad fork to 11375214 (Cargo.toml fix)
Some checks failed
nigig-build.yml / Update makepad fork to 11375214 (Cargo.toml fix) (push) Failing after 0s
pdf.yml / Update makepad fork to 11375214 (Cargo.toml fix) (push) Failing after 0s
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
- Fixed TOML parsing error where fork-specific dependencies were in wrong section - Dependencies now correctly placed in [dependencies] before [features] - Maps feature should now be properly recognized |
|||
| 8c9ccb92cc |
Update makepad fork to latest dev branch (d6d1f99c)
Some checks failed
nigig-build.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
pdf.yml / Update makepad fork to latest dev branch (d6d1f99c) (push) Failing after 0s
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
- Sync with upstream commit 5d4483f (latest map improvements + platform updates) - Include location API, audio echo cancellation, bridge-dz overlay - Add new libraries: geodata, map_nav, i_float, i_shape, i_tree, converse, llama vision - Preserve all fork-specific re-exports (gltf, csg, test) - All 102+ map improvements now available: 2D/3D toggle, shadows, labels, overlays, pattern fills |
|||
| bea1fd884e |
chore: update makepad fork to include upstream map improvements
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-map / test (push) Has been cancelled
Payment domain, storage, platform and UI / isolated-payment-tests (push) Has been cancelled
Payment domain, storage, platform and UI / payment-ui-tests (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Updated all makepad dependencies from rev 2c5cd97 to 817d881 which includes: - Terrain hillshade landcover draping (drape.rs) - Route overlays, markers, and position puck (overlay.rs) - Map icon management system (icons.rs + 50 SVG icons) - 3D road elevation and seamless joins - Building shadow geometry and terrain shadows - Night themes and emissive roads - Water, grass, and shrub rendering - Optimized road geometry with 2D/3D mode transitions - i_overlay library for polygon boolean operations This brings nigig-map in sync with the latest makepad dev branch improvements. |
|||
| 60db0f21db |
build: update Nigig to Makepad dev reexport fork
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
|
|||
|
|
b5471e32e3 |
fix(cad): make the crate buildable, testable and safe to ship
Some checks failed
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
Phases 0-2 of CAD_ASSESSMENT_AND_PLAN.md. The crate did not compile and no
test had ever run; it now builds clean with a green suite.
Build and CI (Phase 0)
- Pin all 33 git dependency manifests to an explicit rev. A branch
dependency re-resolves on every build and is a code-execution path into
CI if force-pushed.
- Commit Cargo.lock (540 packages). Producing it required fixing three
resolution failures the workspace had always had: a non-existent
makepad-widgets feature, two rusqlite versions both linking sqlite3, and
four missed CellId call sites in spreadsheet-ui.
- Add .forgejo/workflows/nigig-build.yml.
- Replace five stale CAD docs that contradicted the code with one
ARCHITECTURE.md; add PHASE0/1/2_STATUS.md and TEST_BASELINE.md.
Correctness (Phase 1)
- Rotation units: transform_point bound sin_cos() backwards, transposed X
and Z, and applied axes in reverse order, so every exported STL was wrong
even at zero rotation. It now shares the renderer's matrix helpers.
- GLB quaternions had norm 0.125 (half-angle applied to cos/sin, degrees
read as radians) - invalid per the glTF spec.
- PDF wall/door/window yaw fed degrees to cos/sin.
- Fix a TOCTOU unwrap in touch picking; viewport.rs now has no unwrap().
- CommandContext gains update_node/insert_node_at/node_index: resize and
modify were delete+create, silently moving nodes to the end of the scene.
- Wire MAX_UNDO_LEVELS (defined, exported, never read) and switch the undo
stack to VecDeque; this also made the existing drag-merge logic reachable.
- CadNode::size() returned a fake 1x1x1 for CSG and extruded solids, making
them unpickable outside a 1x1x1 box at their origin.
- Reject non-finite script input; makepad_csg clamps NaN rather than
propagating it, so bad input produced silently wrong geometry.
Test baseline: 0 -> 722 passing, 0 failing
- 17 pre-existing failures fixed: 10 real defects (dependency-cycle
detection, over-allocation of unassigned tasks, quote/backslash
corruption on save, default rooms lost for all but the first region,
RGA text ordering) and 7 tests that were themselves wrong, each checked
against its production caller first.
Security (Phase 2)
- env!("CARGO_MANIFEST_DIR") was used as a runtime path in three places,
including as the AI agent's working directory. All runtime data now goes
under app_data_dir().
- Remove the hardcoded LAN LLM endpoint. It is now opt-in via
NIGIG_CAD_LOCAL_OPENAI_URL/_MODEL and refuses plaintext HTTP to anything
but loopback.
- Bound and content-sniff AI image attachments (8 MB cap, magic bytes);
the MIME type came from the filename extension.
- Escape SVG/HTML output, and add SRI to the exported viewer's script tag.
The pinned model-viewer@3.5.1 does not exist, so every exported viewer
was silently broken; now 4.0.0 with a verified hash.
- Stop embedding $USER in exported PDFs and logging document content in
release builds.
- CI now rejects reintroducing the runtime-path and hardcoded-endpoint
classes; both gates were verified to fail on a reintroduced defect.
Add system_prompt.md and embed it with include_str!. The file was missing
from the repository, so the agent silently used a one-line fallback.
|
||
| 62dada264e | build: consume Makepad sibling APIs through widgets | |||
| e73def6d1b | build: align Makepad dependencies with Robrix fork | |||
| 5af8a20d04 | build: use Robrix upstream Robius dependencies | |||
| f6750c8259 | feat(pay): add domain coordinator and sqlite storage foundation | |||
| cc05abdc71 | Initial commit |