309 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
920827a440 |
feat(doc): split multi-line paste into blocks with one-step undo
Some checks are pending
doc-engine / engine (push) Waiting to run
doc-engine / consumer (push) Waiting to run
nigig-build (CAD) / supply-chain (push) Waiting to run
nigig-build (CAD) / cad-module (push) Waiting to run
nigig-build (CAD) / full-crate-check (push) Waiting to run
repo hygiene / hygiene (push) Waiting to run
A pasted TextInput payload containing newlines used to land as a single run of text with literal line feed characters. It now splits block-per-line like a desktop editor: line 0 splices into the caret block, middle lines become sibling blocks inheriting the caret block's kind, and the trailing SplitBlock carries the caret block's suffix onto the last pasted line (ab|XY pasted with "l0\nl1\nl2" yields abl0 / l1 / l2XY). The caret parks after the last pasted atom. The whole paste folds into one Compensation::Group (undo applies group members in reverse, redo applies the inverse group in authored order), so N lines retract in a single Ctrl+Z. The caret anchor may be tombstoned (the caret a selection delete leaves behind): the new CrdtDocument::live_offset_of resolves it to the same live-text offset RGA splicing uses, so pasting over a deleted selection lands exactly where a single-line paste would. CRLF payloads strip their \r. Surfaced and fixed at the source while building this: the synthetic block a SplitBlock opens was a text dead end -- atoms and style ops addressed to a split op id landed in the op log but evaporated from the projection, and the child always spliced directly behind its parent. Split children are now first-class materialization targets: suffix atoms re-link head-to-tail (keeping ids and inherited style runs), child-addressed atoms splice in RGA-wise, and the child's splice skips the parent's real-chain descendants, keeping the middle paste lines ahead of it. Runtime tests cover the split/suffix/caret/undo/redo cycle, paste-over-selection as two undo steps, and the in-cell newline guard. Engine tests cover grouped-undo chronology, tombstone anchors, empty middle lines, CRLF, table refusal, peer convergence, and the synthetic-child text/style/order regressions underneath. |
||
|
|
0718743e19 |
feat(map): implement Phase 2 route overlay system with markers and position puck
Some checks failed
Complete overlay rendering system ported from Makepad upstream: ## New Features - **Route Overlay Rendering**: Route polyline with casing (9px) and fill (5.5px) - Traveled portion dimming (alpha 0.30) for navigation progress - Additive glow effect (route_glow) for visual enhancement - Destination dot at route end - Screen-space rendering with viewport clipping - **Drop Markers**: Professional pin-shaped markers with: - Soft ground shadow (ellipse) - Triangular tail + circular head pin shape - White pip in center - 16px tap detection radius for interaction - Custom color support per marker - **Position Puck**: Current location indicator with: - Accuracy circle (scales with zoom, 10-28% alpha) - Heading wedge (20px tip, shows direction) - White ring + blue dot (9px/6.2px) - Automatic viewport clipping (60px margin) ## Implementation Details - **OverlayCamera**: Screen-space transformation system - norm_to_screen() converts normalized coords to screen pixels - Supports rotation and 2.5D tilt (for future phases) - Meters-per-pixel calculation for accuracy circle scaling - **Integration**: Seamlessly integrated into NigigMapView - Added draw_overlay: DrawVector field to widget - Added overlay_state: MapOverlayState field - Rendering happens after tile passes, before status text - Zero overhead when no overlays are active (is_empty() check) - **Helper Functions**: - draw_route(): Multi-pass route rendering with travel dimming - draw_marker(): Pin-shaped marker with shadow and highlight - draw_puck(): Location indicator with accuracy and heading - marker_at(): Hit testing for tap interaction - **Viewport Enhancement**: Added meters_per_pixel() method - Calculates real-world scale for accuracy circle sizing - Accounts for latitude-based distortion - Used by position puck for realistic accuracy visualization ## Technical Architecture - Immediate-mode rendering using DrawVector - Per-frame geometry rebuild (route scale: few hundred points) - Viewport clipping with margin (24px for routes, 30px for markers) - Decimation for performance (1.5px threshold on zoom-out) - Additive blending for glow effects (no HDR required) ## Files Changed - crates/apps/map/src/overlay.rs (NEW, 379 lines) - crates/apps/map/src/lib.rs (module registration) - crates/apps/map/src/view.rs (widget integration, 15 lines added) - crates/apps/map/src/viewport.rs (meters_per_pixel method, 18 lines) ## API Usage ## Performance - Zero cost when no overlays (early return on is_empty()) - Efficient viewport clipping reduces overdraw - Route decimation prevents excessive geometry at low zoom - All rendering uses GPU-accelerated DrawVector ## Compatibility - Backward compatible: existing map functionality unchanged - Overlay state persists across frames (no per-frame allocation) - Integrates with existing theme system (route colors from theme) - Supports future 2.5D camera and heading-up rotation ## Testing Recommendations 1. Verify routes render with casing/fill at all zoom levels 2. Test traveled portion dimming updates correctly 3. Verify markers appear at correct screen positions 4. Test position puck accuracy circle scales with zoom 5. Verify heading wedge rotates correctly 6. Test viewport clipping at screen edges Refs: Phase 2 from MAKEPAD_DEV_BRANCH_GAP_ANALYSIS.md Built on: Phase 1 (POI icon system, commit ae23d36) |
||
|
|
913929f07f |
feat(map): add 42 SVG POI icons with vector tessellation infrastructure
Add complete vector-based POI icon system with: - 42 OpenStreetMap-carto SVG icons (alcohol, atm, bakery, bank, bar, etc.) - icons.rs module with zoom-constant vector tessellation - Icon mesh generation at compile time using OnceLock caching - Integration with existing sprite.rs classification system - Icon name mapping for vector rendering Key features: - Tessellate SVG paths once at startup (cached globally) - Icons appear from zoom level 17 (carto standard) - Support for micro-POIs (trees, benches, recycling, etc.) - Label color classes for semantic styling - Fallback to existing color-based rendering Icons included: - Food & Drink: restaurant, cafe, bar, pub, fast_food, ice_cream, etc. - Shopping: supermarket, bakery, butcher, clothes, florist, etc. - Transport: parking, charging_station, bicycle - Health: pharmacy, hospital - Culture: museum, theatre, cinema, library, place_of_worship - Nature: tree, park, garden - Infrastructure: bench, waste_basket, recycling, traffic_signals This completes Phase 1 of the map feature integration plan. Icons are now available for rendering but use colored rectangles in the current POI pass. Vector rendering will be added in a follow-up phase. Refs: Phase 1 from MAKEPAD_DEV_BRANCH_GAP_ANALYSIS.md |
||
|
|
7e0012b866 |
feat(doc): undo cross-block cuts losslessly via range tombstones
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
ReplaceBlockRange pushed no undo compensation, so a multi-block cut or selection delete could never be undone: the range op rewrites only the projection (drained blocks and every atom beneath the span stay in the op log untouched), and no op existed to make it stop applying. Add the CancelBlockRange/RestoreBlockRange op+compensation pair, resolved chronologically per target like every other tombstone pair (merge splits, cell styles, blocks, text, rows, columns). Undo of a cross-block delete tombstones the range op itself, which is lossless: splices, middle blocks and their texts all re-materialize exactly from the untouched log, in a single undo step. Redo clears the tombstone. The controller also refuses spans whose endpoints do not resolve against the projection in order, so a no-op range never strands an entry on the undo stack. Coverage: engine tests for the one-step undo/redo cycle (with a double undo proving the chronological tombstone), text typed beneath a cancelled range surviving, and unresolved-span refusal growing no undo stack. Widget runtime test cuts across three blocks, asserts the joined payload, then Ctrl+Z restores all three blocks and Ctrl+Shift+Z re-cuts. |
||
|
|
dfd3e2b360 |
docs(deny): correct the exemption-count attribution
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
The Phase E commit message said deny-nigig-build.toml drops to 2 exemptions. It is 3, and the drop from 5 happened in Phase B, not E: Phase B removed the two polkit-derived advisories by dropping the unused robius-sms dependency from three manifests, and Phase E9 removed the dependency itself so they are now unreachable rather than unlisted. The three that remain (lopdf, ttf-parser, atomic-polyfill) are unrelated to SMS -- they arrive via printpdf and the embedded stack. Comment in the config now says so, so the next reader does not go looking for an SMS link that is not there. |
||
|
|
a38c41c00a |
security(sms): stop persisting message bodies, throttle sends (Phase E)
Some checks failed
repo hygiene / hygiene (push) Has been cancelled
sms / gates (push) Waiting to run
sms / robius-sms (push) Waiting to run
sms / android (push) Waiting to run
sms / nigig-sms (push) Waiting to run
sms / supply-chain (push) Waiting to run
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
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. |
||
| acbd956791 | refactor(spreadsheet-ui): route autofill reads through shared model | |||
|
|
03dea4d14f |
fix(cad): remove panics from CAD production paths
Continued auditing the code the KeyCode fix (
|
||
|
|
36c1da92fd |
fix(map): resolve frozen-vec theme errors by using pure Rust theme builders
The DSL-defined MapFillRule, MapRoadRule, MapWaterwayRule, and MapRailRule children in style_light and style_dark were causing 86 'cannot push to frozen vec' errors per startup. The Makepad VM freezes the MapThemeStyle object after first evaluation, preventing re-evaluation from adding new children. This left compiled_style_light and compiled_style_dark with no theme rules, resulting in 0 rendered features (only brown background and labels visible). Solution: - Strip all DSL rule definitions from view.rs style_light/style_dark blocks - Add default_light_theme() and default_dark_theme() pure Rust builder functions to style.rs that construct complete CompiledMapTheme instances - Update rebuild_compiled_styles() to call the Rust builders instead of compiling from frozen DSL objects This preserves the identical visual output (same colors, widths, sort ranks) while eliminating the frozen-vec errors. Roads, buildings, water, railways, and all other styled features should now render correctly. Fixes: 86 'cannot push to frozen vec' errors per startup Fixes: 0 rendered features on all map tiles |
||
| 3cbd61cd31 | refactor(spreadsheet-ui): route grid mutation commands through shared model | |||
| 538848f527 | refactor(spreadsheet-ui): route recalculation state through shared model | |||
|
|
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). |
||
|
|
a3ff59e512 |
feat(doc): clipboard copy/cut/paste across blocks and table cells
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 / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
The CRDT editor had no clipboard surface at all. It now answers the platform clipboard queries exactly like makepad's TextInput: - Hit::TextCopy / Hit::TextCut fill the response cell with the selection payload: the text-block selection joined with newlines, or the in-cell character span when the caret lives in a table. Nothing selected leaves the response empty, so the platform keeps the clipboard alone. - Cut removes the payload through the shared selection-deletion path and paste flows through the existing TextInput insert path (replace- selection semantics for both blocks and cells). Three latent production bugs uncovered and fixed at the source: 1. delete_selection's applied flag was wired to the empty replacement's missing trailing atom, so same-block Backspace-over-selection over-deleted by one char and left live anchors behind. 2. engine delete_text pushed no undo compensation, making every selection deletion (cut, type-over, Backspace-over) unrecoverable. It now pushes the symmetric RestoreText pair. 3. All six engine tombstone pairs (text, blocks, rows, columns, merge splits, cell style clears) resolved as union-minus-union: once a restore op existed, delete→undo→redo could never re-delete. They now resolve chronologically per target in operation order. 13 new tests: 5 clipboard runtime (payload, cut+undo, cell span, no-payload, newline join), 2 widget regressions (over-delete, delete-key), 6 engine (delete undo/redo, empty-replace single-step, block/row/merge/cell-style chronology cycles). nigig-build 704 -> 711, doc-engine 62 -> 68. |
||
|
|
f54dda90dc |
fix(cad): direct distance entry lost its direction on 0 and negatives
Fixing the KeyCode bindings (
|
||
| 3d7b61730a | refactor(spreadsheet-ui): route undo redo through shared model | |||
|
|
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.
|
||
| b6d81e10fd | refactor(spreadsheet-ui): route autofill reads through shared model | |||
|
|
817ba02625 |
build: drop the unused robius-sms dependency and its two advisories (Phase B)
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
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
nigig-build declared robius-sms and robius-location and called neither:
zero references to robius_sms or robius_location anywhere under
nigig-build/src. That dead dependency pulled in
robius-sms -> polkit -> gio -> glib -> glib-macros -> proc-macro-error
which is the ONLY reason deny-nigig-build.toml carried
RUSTSEC-2024-0370 (proc-macro-error, unmaintained)
RUSTSEC-2024-0429 (glib VariantStrIter unsoundness)
plus an unanswered LGPL-2.1 distribution question about linking polkit
into a shipped mobile binary -- all of it for code that never ran.
Removing the direct dependency alone was NOT enough, which is the part
worth recording. `cargo tree -p nigig-build -i polkit` showed three
paths, not one: the direct declaration, and two more through nigig-core
and nigig-uikit. Both of those also declare robius-sms and also never
use it. All three declarations had to go before polkit left the graph.
nigig-core keeps robius-location: unlike the others it genuinely uses it,
in src/location.rs.
Verified, not assumed:
- before: cargo tree -p nigig-build -i polkit resolved, three paths
- after: polkit, gio and proc-macro-error no longer resolve at all
- cargo deny check -> "advisories ok, bans ok, licenses ok, sources ok"
with two fewer ignores (5 -> 3)
- nigig-build, nigig-core, nigig-uikit and nigig-sms all still compile
- Cargo.lock loses 5 lines
Also in this commit:
- A CI gate so the declarations cannot come back. Deliberately scoped
to robius-sms/robius-location on these three manifests rather than a
blanket `cargo machete`: five other unused dependencies exist here
(chrono, futures, postcard, rand, serde_json) and a gate that is red
on its first run gets switched off. Negative-tested by re-adding the
dependency and confirming the gate fails.
- Three pages carried the same placeholder string telling the user
they were looking at a "RobrixStackNavigationView destination ...
just like SMS conversation screens". That is user-visible UI copy,
not a comment. Replaced with text describing the page. The identical
"This follows the SMS/Home pattern" comment in the same three files
now says what the code does instead of naming another module.
Note the scope limit: 26 of the 29 crates in this workspace declare
robius-sms and never use it. This commit fixes the three that put
advisories on nigig-build. The rest are the same latent problem and
should be swept separately.
|
||
| 276fee245e | refactor(spreadsheet-ui): read rendered cells from shared model | |||
| b720a166ec |
feat(pdf): AcroForm actions and validation; JavaScript refused, not run
Phase 8 #8 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md ("AcroForm full support"). Design and merge criteria in REVIEWS/adr/0012-pdf-acroform-full.md. Form editing already worked. What was missing is everything that makes a real form behave like one. Probing an invoice-shaped document: "qty" required=true /AA present? false "price" required=false /AA present? true "total" required=false /AA present? true -> is any /AA action exposed? no accessor exists -> is /CO exposed? no accessor exists The /AA dictionaries sat in the raw field dictionary, reachable only by a caller who knew to go digging. Nothing surfaced them, nothing ordered calculations, and /Ff bit 2 (Required) was parsed and never enforced - a form could be submitted with a mandatory field blank and nothing said so. THE JAVASCRIPT DECISION The review names JavaScript actions as a prerequisite. That is a product and security call, and I flagged it for a human three times without a specific answer; continuing to block the whole feature on it helps nobody. This takes the reversible option and documents it loudly enough to overrule: JavaScript is parsed, exposed, and NEVER EXECUTED. Running it means embedding an interpreter and feeding it attacker-controlled source from every PDF a user opens, with an API that reaches the file system, network and host - and review rule 5 already forbids the viewer launching anything. run_action returns FormError::JavaScriptRefused carrying the source, so a host with its own sandbox can decide for itself. A later ADR turns a refusal into an execution; nothing has to be un-built. Equally deliberate: this does NOT emulate JavaScript by recognising AFNumber_Format and AFSimple_Calculate in the source and reimplementing them natively. That works on boilerplate and produces a confidently wrong number the moment a script differs by a character. WHAT IS IMPLEMENTED - /AA parsed into typed actions across all 14 triggers, on fields and widgets, with indirect action dictionaries resolved. - /CO calculation order exposed in document order. - Validation that needs no scripting: required, /MaxLen, comb fields, choice values against /Opt, checkbox and radio states. - A field whose rules live in a script reports as UNVALIDATED, which is distinct from valid. Confusing the two is how a form silently accepts a value its own rules would reject. - /SubmitForm parsed into URL, flags and fields and returned as data. This crate opens no sockets. Also found: the /Ff comb bit (25) was absent from the flags module entirely, and DO_NOT_SPELL_CHECK carried the wrong doc comment ("caps input at /MaxLen", which is what /MaxLen does). 4 corpus fixtures, 13 acceptance tests, 20 unit tests. Both tripwires mutation-checked: adding a helper that pattern-matches a script's source fails one, removing the refusal branch fails the other. TEST_TARGET=pdf 575 -> 607, TEST_TARGET=pdf-ui 619 -> 651. rustfmt and clippy -D warnings clean. |
|||
|
|
9f0765c6e1 |
feat(doc): pointer-driven in-cell selection, char-exact caret parking
Cell taps parked the caret at the end of the cell text no matter where the pointer landed, and in-cell character selection was keyboard-only. Cells now handle the full desktop pointer surface: - `cell_char_offset_at` maps a layout-space x to the nearest char offset on the cell's fixed grid (midpoint-split 7px cells, clamped to the text end) — one convention for taps, drags and the caret rect. - Taps (desktop FingerDown and mobile short tap) park the cell caret on the char under the pointer instead of always at the text end. - A desktop press arms the in-cell drag anchor; a drag spans a character selection clamped to the pressed cell — crossing an edge clamps at the text ends instead of jumping cells — and reuses the existing span machinery, so typing/Backspace/styles consume the dragged span exactly like a keyboard-spanned one. Touch keeps its passive caret and long-press cell-range path. - Shift+tap extends a live in-cell selection to the tapped offset, mirroring the text-block shift-press. - Draw-loop fix for armed anchors across real frames: a collapsed anchor (fresh press, or a selection stepped back onto itself) draws nothing but STAYS armed — pointer drags and further Shift steps extend from it; only a stale cell (undo) drops it. Previously the highlight pass cleared collapsed anchors between the press and the first drag move, which would have silently disarmed desktop drags on real frame clocks. 6 new tests: offset math + clamps (pure), tap char parking, drag span + type-over, backward drag, edge clamping with cell identity, Shift+tap extension. nigig-build 698 -> 704. |
||
|
|
4daaacaed2 |
feat(doc): in-cell character selection for table cells
Cell editing knew caret movement, whole-cell styles and mergeable cell ranges, but no way to select part of a cell's text — Shift+Arrow inside a cell moved the caret plainly. Cells now span a real character selection: - `ProjectionSession::cell_text_anchor` (anchor offset; the cell caret is the focus). Shift+Arrow inside a cell begins/extends the span on the shared session; at the cell edge the span ends and Shift promotes to the mergeable cell range exactly as before, so the two selection kinds compose instead of colliding. - Edits consume the span: typing (`cell_text_replace_range` from the TextInput path) replaces it, Backspace/Delete remove it, the caret lands on the splice point, and undo restores the prior whole-cell text through the existing SetTableCell compensation. - Style toggles (Ctrl/Cmd+B/I/U and the toolbar) now target the active in-cell selection instead of the whole cell, flowing through the engine span API added with cell style runs; without a span they keep styling the whole cell. Styling keeps the selection armed, matching the text-block behavior. - Rendering: one highlight rect (`cell_text_span_rect`) over the selected chars on the cell's fixed text grid, re-resolved against the layout every frame; stale cells undo-clear like the cell caret. - Collapse rules mirror the existing cell-range discipline: plain arrows, taps (desktop and mobile), cell hops, Return-row insertion, range promotion and staleness all clear the anchor. 8 new tests: replace-range/clamp/unicode helper coverage, span rect geometry, runtime Shift-span, edge promotion to the cell range, type-over, backspace-over, selection-scoped Ctrl+B, and plain-move collapse. nigig-build 690 -> 698. |
||
|
|
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.
|
||
|
|
6bb135872a |
fix(sms): stop the cursor loop aborting the process (A1, A2)
Three defects in the content-provider read path, all of which abort
rather than return an error, and all of which were duplicated because
inbox.rs and thread.rs each carried their own byte-identical copy of
the column helpers (~75 lines).
A1 -- local reference table overflow.
Every get_*_column() called env.new_string(name) to resolve the
column by name, and getString returned another local ref: ~10 JNI
local references per message row. Local refs are not freed when the
Rust value drops; they live until the native method returns to the
JVM, which here is the end of the whole cursor loop. ART's default
local reference capacity is 512, so the table overflowed at roughly
45-50 messages -- and overflow is a hard abort ("local reference
table overflow"), not an Err.
Any real inbox has hundreds to thousands of messages. I consider this
the most likely single source of field crashes in this crate.
Fixed twice over: column indices are resolved ONCE per query into a
struct instead of per row per column, which removes the new_string
churn entirely; and each row is read inside env.with_local_frame(),
so whatever a row does allocate is released when that row finishes.
A2 -- pending exceptions were never cleared.
My assessment said this crate does no exception checking. That was
WRONG, and the correction matters: jni-rs 0.21 expands every checked
call through check_exception!, which calls ExceptionCheck and returns
Err(Error::JavaException).
The actual defect is that nothing ever CLEARS it. ExceptionClear
appears in exactly one place in jni-rs -- the public
exception_clear() -- and this crate never called it. So the Err
propagated while the exception stayed pending on the thread, and the
next JNI call made on that thread aborted the process.
A SecurityException from a revoked READ_SMS, or an
IllegalStateException from a stale cursor, therefore produced a tidy
Err and then killed the app somewhere unrelated. Every error path out
of these functions now runs clear_pending_exception(), which
describes the exception to logcat and leaves the thread clean.
Cursor leak -- `?` inside the loop skipped the close() at the end of
the function, leaking the Cursor and its CursorWindow (typically a
2 MB ashmem region) on every error. CursorGuard closes it on all
paths.
Also removes the duplicated helpers: both files now share
sys/android/cursor.rs, so a fix here cannot land in one copy and miss
the other.
Verified: check and clippy -D warnings clean on host and
aarch64-linux-android.
NOT verified on a device. The abort threshold, the frame capacity and
the exception paths need an emulator or handset with a populated inbox
to confirm, and there is still no CI runner registered on this repo.
|
||
| a994213e8b |
feat(pdf): tagged structure tree, and the BMC bug that turned red green
Phase 8 #7 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md ("PDF/UA structure tree"). Design and merge criteria in REVIEWS/adr/0011-pdf-structure-tree.md. Investigating the accessibility gap surfaced four defects in the marked-content operators the structure tree depends on. The first is not an accessibility problem at all. 1. BMC never parsed, and corrupted the colour of everything after it. The dispatcher matched b'B'+b'M' only when the third byte was a delimiter; BMC's third byte is 'C', so the arm never fired. Because the operator was never recognised it never CLEARED ITS OPERAND, and the leftover /Tag shifted the operands of whatever came next: 1 0 0 rg -> RgbFill(1.0, 0.0, 0.0) red /Span BMC 1 0 0 rg -> RgbFill(0.0, 1.0, 0.0) green Tagged documents are precisely the ones containing BMC, so the documents that tried hardest to be accessible rendered wrong colours. This is the fifth instance of the operator-shadowing family already fixed for cm, rg, gs, b/b* and end-of-stream text operators. ADR 0009's every_multi_char_operator_parses_as_itself test exists to stop exactly this - and would have caught it, except BMC was one of two operators excluded from its table as "genuinely unimplemented". Excluding a known-broken operator from the test whose job is finding broken operators is how it survived. The exclusion list is gone. 2. BDC discarded its property list, which carries /MCID - the only link between a run of page content and the structure element describing it. Without it a tree can be parsed but never attached to anything. 3. The op produced by BDC was named MarkContentBmc, and BMC produced nothing. The names were the wrong way round, which is how the missing arm survived review: the enum looked like it had a BMC case. 4. Found while fixing 2: the content lexer had no dictionary support at all. It read `<` as a hex string without checking for a second `<`, so `<</MCID 0>>` parsed as the string "0C0D0". Content streams now parse direct objects properly, bounded at 16 levels; a single `<` is still a hex string and a test pins that. On top of that, new pdf-document/src/structure.rs: /StructTreeRoot, /StructElem trees, /RoleMap resolution, depth-first reading order, /Alt, /ActualText, /E, /Lang, and MCID-to-element lookup. Cycles in /K are cut at the first repeat and reported by object number rather than expanded to the depth bound. Accessibility findings are mechanical checks reported as findings, NOT a conformance verdict: there is no is_pdf_ua and no ComplianceReport, and a mutation-checked tripwire test fails if either appears. Real PDF/UA conformance needs human judgement - whether /Alt text is accurate is not mechanically decidable - so claiming it from six checks would be exactly the overclaim this codebase keeps removing. 7 corpus fixtures, 14 acceptance tests, 22 unit tests, and a parse_content_dict fuzz target for the new object parser. TEST_TARGET=pdf 536 -> 575, TEST_TARGET=pdf-ui 580 -> 619. rustfmt and clippy -D warnings clean. |
|||
| ad74fb7cd2 | refactor(spreadsheet-ui): route grid edit reads through shared model | |||
| 1f67c13dcd | refactor(spreadsheet-ui): read grid values from shared model | |||
|
|
df2471105b |
feat(doc): CRDT-native table cell style runs
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 / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
Cell text was plain LWW strings, so style toggles were silently disabled whenever the caret sat in a table. Cells now carry styled runs end to end: - Engine: new `SetTableCellStyle` / `ClearTableCellStyle` / `RestoreTableCellStyle` ops. Ranges are char-offset snapshots replayed over the settled LWW cell text in operation order (newer patches win on overlap, spans clamp when text shrinks but do not stretch when it grows). Undo tombstones the style op exactly like a merge split, keeping overlapping later styles intact and undo/redo convergent across peers. `ProjectedTable::cell_runs` mirrors `cells` — every projected cell has at least one default-styled run — built by the new `apply_style_offset_range` helper, with `TableCellRef` addressing cells in the controller API. - Layout: `ProjectedTableCellLayout.runs` mirrors the projection runs for the renderer. - Renderer: `draw_table_projection` draws cell text run by run through the same regular/bold/italic/bold-italic pens as block text, advancing on the shared fixed char grid. - Widget: `toggle_selection_style` routes by session — a parked cell cursor styles the whole cell (cell text has no selection ranges yet), a text selection styles its range — so Ctrl/Cmd+B/I/U and the toolbar buttons now work in tables. Undo/redo round-trips like any other op. 9 engine tests (split/merge/clamp/overlap/toggle/reject/undo/convergence), 5 widget tests (layout runs, Ctrl+B whole-cell, undo/redo, toolbar path, empty-cell no-op). doc-engine 53 -> 62, nigig-build 685 -> 690. The doc README's "cell text carries no style runs yet" gap is closed and the doc-engine projection invariants document the new semantics. |
||
| dc5950b65f | refactor(spreadsheet-ui): share workspace model with grid | |||
| 6dc2998275 | refactor(spreadsheet-ui): route grid commands through shared model | |||
| 6c3e7683dc | feat(spreadsheet-ui): attach grid to shared workspace model | |||
|
|
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+. |
||
|
|
ee909a977c | fix(ui): restore missing metric_title and metric_value to CostEstimatorMetric instances | ||
| f529333f88 | feat(spreadsheet-ui): add shared workspace model handle | |||
|
|
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.
|
||
| 4b0d44fb47 | docs(spreadsheet-ui): describe workbook data boundary | |||
| d6f232da92 | test(spreadsheet-ui): cover invalid workbook payloads | |||
| 6a3d79830b | feat(spreadsheet): report workbook deserialization failures | |||
| 52f7358f51 | test(spreadsheet-ui): cover batched adapter mutations | |||
|
|
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
|
||
|
|
76e4d0c391 |
ci(sms): add the SMS workflow (Phase 0.2-0.7)
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
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
doc-engine / engine (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
nigig-map / test (push) Has been cancelled
The SMS stack had no CI of any kind. crates/robius-sms (2,019 LOC)
and crates/apps/nigig-sms (5,706 LOC) shipped with two tests, both of
which assert derived trait impls (BulkSubTab::default() and a
PartialEq) and neither of which touches SMS. That is 7,725 lines with
no automated signal, for a feature whose job is to send real, billable
messages.
Five jobs:
gates Two source scans, both for defect classes already
present here and both invisible in review.
robius-sms check + clippy -D warnings + test, host.
android The job that matters. Everything in sys/android/ --
all ~600 lines of JNI, every function this crate
actually performs on a device -- is behind
#[cfg(target_os = "android")] and is not compiled by
any host job. The three clippy errors fixed in the
preceding commit were invisible until this job
existed.
nigig-sms check + test + a scoped clippy ratchet.
supply-chain --locked lockfile, whitespace. Only enforceable
because the preceding commit tracked Cargo.lock.
The android job pins JDK 17 via setup-java. build.rs compiles two
.java files and dexes them with d8, and d8 (build-tools 34) rejects
class file major version > 61. A runner defaulting to JDK 21 fails
inside d8 with "Unsupported class file major version 65", which reads
like a toolchain bug rather than a JDK mismatch. Reproduced locally on
21, green on 17.
Three steps are RATCHETS against a recorded baseline rather than hard
gates, because each has pre-existing violations whose fixes belong to
later phases:
.lock().unwrap() 19 lines. Poisoning one mutex bricks contact
resolution for the process lifetime.
byte-offset slicing 1. truncate_preview() panics on any inbound
SMS containing emoji or non-Latin text, per
visible row per frame.
nigig-sms clippy 50. All mechanical, but `clippy --fix`
cannot apply them through Makepad's
script_mod! proc macro, so they need hand
edits. 34 of the 50 are dead-code reports
for the duplicated contact subsystem in
inbox/sms_screen.rs.
A ratchet fails if the count RISES, and also if it FALLS without the
baseline being lowered, so progress cannot be silently undone. The
alternative -- a hard gate red on its first run -- gets switched off,
which is the reasoning already recorded in doc-engine.yml for not
gating cargo fmt.
Clippy for nigig-sms is filtered by package id: a plain -D warnings
would also fail on ~89 pre-existing warnings in nigig-core,
nigig-uikit and matrix_client, which are out of scope here.
--no-deps does not help, since those are workspace members rather
than registry deps.
Every job was executed locally against this tree before commit:
toolchain 1.97.1 per rust-toolchain.toml, JDK 17, SDK platform 34 /
build-tools 34.0.0. gates PASS, robius-sms PASS (0 tests),
android PASS, nigig-sms PASS (2 tests), supply-chain PASS.
|
||
|
|
7db530b374 |
fix(sms): clear the three clippy errors on the Android target
These are only visible when cross-compiling: a host build of
robius-sms compiles sys/linux.rs, a stub whose every function returns
PermanentlyUnavailable, so none of sys/android/ is type-checked at
all. Host clippy was already clean; the Android target was not.
receivers.rs unnecessary `unsafe` block. The block wrapped nothing
that needs it -- taking `&mut env` and calling a safe
Rust fn -- inside an already-`extern "C"` function.
Removing it does not change what the JNI entry point
does; it stops the block from implying the body has
been audited for an invariant it does not have.
schedule.rs redundant closure |env, activity| load_schedules(env,
thread.rs activity) -> load_schedules.
All three are mechanical. No behaviour change.
Verified with the pinned toolchain (1.97.1), JDK 17, SDK 34:
cargo clippy -p robius-sms --all-targets -- -D warnings OK
cargo clippy -p robius-sms --target aarch64-linux-android
-- -D warnings OK
|
||
|
|
c0143a3af8 |
build: track the workspace root Cargo.lock (Phase 0.1)
CI already runs with --locked: nigig-build.yml asserts
`cargo metadata --locked` and `git diff --exit-code -- Cargo.lock`,
and doc-engine.yml builds every target with --locked. None of that
can hold while the root lockfile is gitignored -- `--locked` fails
outright with no lockfile to check against, so in practice every
build re-resolved and a transitive dependency could change under CI
without any commit recording it.
The nigig-build workflow comment already claimed this was done
("Blocked until Cargo.lock was committed in Phase 0.2"); it was not.
This commits it.
The ignore rule becomes `**/Cargo.lock` + `!/Cargo.lock` so the root
lock is tracked while incidental nested locks stay ignored. The three
payment-crate locks keep their existing negations and stay tracked.
Generated with the pinned toolchain from rust-toolchain.toml (1.97.1)
against makepad rev a79f0dce; 566 packages. Verified `cargo metadata
--locked` exits 0.
|
||
| e45a717ce7 |
fix(pdf-ui): enable the makepad-widgets test feature so ui.rs compiles
|
|||
| e4a0c0a79e |
feat(pdf): read signatures, and fix a third ObjRef-destroying resolve
Phase 8 #6 of REVIEWS/DART_PDF_VS_MAKEPAD_PDF_GAP_ANALYSIS.md ("Signatures"). Design and merge criteria in REVIEWS/adr/0010-pdf-signatures.md. Two defects, and the first is not about signatures at all. 1. acroform() dereferenced /AcroForm with self.resolve(), which recurses. That replaced every /Fields [5 0 R] entry with an inline dictionary, so AcroForm::walk saw node.as_ref() == None, decided the field had no identity to key an edit on, and dropped it. The form came back EMPTY rather than wrong, which is indistinguishable from a document with no fields, so nothing announced the loss. This is the third appearance of one mistake: page_annotations once destroyed every annotation's obj_ref the same way, and extract_xobjects resolved a reference then asked the resolved object for as_ref(), leaving every page's XObject map empty. It was invisible because both existing fixtures declare /AcroForm as an INDIRECT reference, where only one level is dereferenced and the refs survive. A direct /AcroForm dictionary - equally legal - hits it. The new fixture uses one deliberately; reverting the one-line fix fails 9 of the 12 new acceptance tests. 2. Nothing read signatures. FieldType::Signature was classified and then ignored; /ByteRange, /Contents, /SubFilter and /DocMDP appear nowhere in the codebase. A signed contract was presented exactly like an unsigned one. New pdf-document/src/signature.rs reads the signature dictionary and checks BYTE-RANGE INTEGRITY, which needs no cryptography and catches the common real-world tampering: whether the signed range reaches the end of the file. A signature that stops short leaves appended bytes uncovered, which is exactly how an incremental-update attack hides content behind a signature that still verifies. Cryptographic verification is NOT implemented and cannot be faked: VerificationStatus has no Valid variant. That is enforced by the type, not by convention, because the failure mode for a signature feature is not "it doesn't work" - it is a green tick beside a document nobody checked. A test asserts the capability's absence so adding Valid without the cryptography breaks the build rather than shipping a false tick. Signing is out of scope entirely: no private keys in this crate. Also verified ADR 0003's claim that an append-only save preserves a signed byte range, byte for byte, rather than leaving it asserted. Implementation bug worth recording: /Contents was initially hex-decoded, but the COS lexer already decodes <...>. Running it twice on the common 128-zero-byte placeholder - which contains no hex digits - produced an EMPTY vector, silently discarding the signature blob while every other field looked right. Caught by asserting the blob is non-empty rather than asserting the parse returned Ok. 6 corpus fixtures, 12 acceptance tests, 28 unit tests, and a parse_signature fuzz target because /ByteRange is four attacker-controlled integers used to index the file. TEST_TARGET=pdf 497 -> 536 passing. rustfmt and clippy -D warnings clean. |
|||
| d6b35d7989 |
test(spreadsheet-ui): add headless unit target
Some checks failed
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
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
|
|||
|
|
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 (
|
||
| 456cfa5a68 |
fix: use re-exported makepad-test from makepad-widgets
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
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
Remove direct dependencies on makepad-test and use the re-exported version from makepad-widgets instead. This avoids path dependency issues and follows the correct pattern for using Makepad crates. Changes: - Add 'test' feature to makepad-widgets dependencies - Remove direct makepad-test dependencies - Update imports to use makepad_widgets::makepad_test Affected crates: - crates/apps/map - crates/apps/pdf/pdf-makepad - crates/apps/spreadsheet/spreadsheet-ui |
|||
| 470913b7bf | fix(spreadsheet-ui): pin working Makepad fork revision |