Compare commits
3 commits
e45a717ce7
...
76e4d0c391
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76e4d0c391 | ||
|
|
7db530b374 | ||
|
|
c0143a3af8 |
6 changed files with 5839 additions and 7 deletions
341
.forgejo/workflows/sms.yml
Normal file
341
.forgejo/workflows/sms.yml
Normal file
|
|
@ -0,0 +1,341 @@
|
||||||
|
name: sms
|
||||||
|
|
||||||
|
# Phase 0 of the SMS remediation plan: make the SMS stack verifiable.
|
||||||
|
#
|
||||||
|
# Before this file existed, `crates/robius-sms` (2,019 LOC) and
|
||||||
|
# `crates/apps/nigig-sms` (5,706 LOC) had NO CI of any kind and two tests,
|
||||||
|
# both of which assert derived trait impls and neither of which mentions
|
||||||
|
# SMS. That is 7,725 lines of untested code whose job is to spend the
|
||||||
|
# user's money by sending real, billable, irreversible messages.
|
||||||
|
#
|
||||||
|
# Enforces:
|
||||||
|
# 1. Both crates compile on the host AND on aarch64-linux-android.
|
||||||
|
# The host build only ever compiles sys/linux.rs -- a stub whose
|
||||||
|
# every function returns PermanentlyUnavailable. ALL of the real
|
||||||
|
# logic (~600 lines of JNI in sys/android/) is behind
|
||||||
|
# #[cfg(target_os = "android")] and is invisible to a host build.
|
||||||
|
# A host-only gate would be close to worthless here.
|
||||||
|
# 2. Clippy stays clean for code these two crates OWN. Warnings from
|
||||||
|
# path dependencies (nigig-core, nigig-uikit, matrix_client) are
|
||||||
|
# pre-existing and out of scope for this workflow; they are
|
||||||
|
# filtered by package id rather than muted, so they still show in
|
||||||
|
# the log and a future workflow can gate them.
|
||||||
|
# 3. Tests pass.
|
||||||
|
# 4. Two defect classes that are invisible in review and have already
|
||||||
|
# shipped here, as source-scanning gates.
|
||||||
|
#
|
||||||
|
# `cargo fmt --check` is deliberately not a gate, matching the reasoning
|
||||||
|
# already recorded in doc-engine.yml: these crates predate the pinned
|
||||||
|
# toolchain's rustfmt style and reformatting them wholesale would
|
||||||
|
# conflict with the remediation work. A step that always fails gets
|
||||||
|
# ignored, which is worse than no step.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- 'crates/robius-sms/**'
|
||||||
|
- 'crates/apps/nigig-sms/**'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
- '.forgejo/workflows/sms.yml'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'crates/robius-sms/**'
|
||||||
|
- 'crates/apps/nigig-sms/**'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'rust-toolchain.toml'
|
||||||
|
- '.forgejo/workflows/sms.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Source-scanning gates. These need no toolchain, so they run first and
|
||||||
|
# fail fast.
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
gates:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# A poisoned mutex turns every later `.lock().unwrap()` into a
|
||||||
|
# panic. The contact cache in conversations_list.rs is written from
|
||||||
|
# a spawned worker thread, so one panic anywhere under the lock
|
||||||
|
# permanently bricks contact resolution for the process lifetime,
|
||||||
|
# and it surfaces far from the cause.
|
||||||
|
#
|
||||||
|
# The codebase already knows the right pattern -- companies_list.rs
|
||||||
|
# uses `if let Ok(mut s) = ..lock()` -- it is just applied
|
||||||
|
# inconsistently. This keeps the good pattern from regressing.
|
||||||
|
#
|
||||||
|
# RATCHET. There are 19 source lines with these today (bug A6 in the
|
||||||
|
# plan); fixing them is Phase A work, not Phase 0. A gate that
|
||||||
|
# fails on its first run gets switched off, so this asserts the
|
||||||
|
# count never RISES and must be lowered as they are fixed. When it
|
||||||
|
# reaches 0, replace the whole step with a plain grep that fails on
|
||||||
|
# any match.
|
||||||
|
- name: No new .lock().unwrap() in the SMS crates
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
BASELINE=19
|
||||||
|
count=$(grep -rn --include='*.rs' '\.lock()\s*\.unwrap()' \
|
||||||
|
crates/robius-sms/src crates/apps/nigig-sms/src \
|
||||||
|
| sed 's/^[^:]*:[0-9]*://' \
|
||||||
|
| grep -vcE '^[[:space:]]*(//|/\*|\*)' || true)
|
||||||
|
echo "found $count, baseline $BASELINE"
|
||||||
|
if [ "$count" -gt "$BASELINE" ]; then
|
||||||
|
grep -rn --include='*.rs' '\.lock()\s*\.unwrap()' \
|
||||||
|
crates/robius-sms/src crates/apps/nigig-sms/src || true
|
||||||
|
echo
|
||||||
|
echo "ERROR: $count .lock().unwrap() calls, up from $BASELINE."
|
||||||
|
echo "These panic on a poisoned mutex. One panic while the lock"
|
||||||
|
echo "is held bricks the cache for the rest of the process. Use:"
|
||||||
|
echo " if let Ok(guard) = MUTEX.lock() { .. }"
|
||||||
|
echo "or MUTEX.lock().unwrap_or_else(|e| e.into_inner())"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$count" -lt "$BASELINE" ]; then
|
||||||
|
echo
|
||||||
|
echo "Good: down to $count. Lower BASELINE in this file to $count"
|
||||||
|
echo "so the progress cannot be undone."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK"
|
||||||
|
|
||||||
|
# `&s[..n]` where n is a byte offset panics with "byte index N is
|
||||||
|
# not a char boundary" the moment n lands inside a multi-byte
|
||||||
|
# character. truncate_preview() in sms_utils.rs did exactly this
|
||||||
|
# against a 120-BYTE constant, and it runs per visible row per
|
||||||
|
# frame -- so one inbound SMS containing emoji, Swahili or Arabic
|
||||||
|
# text panicked the whole conversation list on every frame. That is
|
||||||
|
# a remote denial of service triggerable by anyone who knows the
|
||||||
|
# victim's number.
|
||||||
|
#
|
||||||
|
# Slicing must go through char_indices()/char_boundary logic.
|
||||||
|
#
|
||||||
|
# RATCHET, for the same reason as the step above: the one live
|
||||||
|
# instance is truncate_preview() in sms_utils.rs, and fixing it is
|
||||||
|
# bug A3 in the remediation plan. Drop BASELINE to 0 with that fix
|
||||||
|
# and this becomes a hard gate.
|
||||||
|
- name: No new byte-offset string slicing in SMS text helpers
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
BASELINE=1
|
||||||
|
count=$(grep -rnE --include='*.rs' \
|
||||||
|
'&[A-Za-z_][A-Za-z0-9_]*\[\.\.[A-Za-z0-9_]+\]' \
|
||||||
|
crates/apps/nigig-sms/src \
|
||||||
|
| sed 's/^[^:]*:[0-9]*://' \
|
||||||
|
| grep -vcE '^[[:space:]]*(//|/\*|\*)' || true)
|
||||||
|
echo "found $count, baseline $BASELINE"
|
||||||
|
if [ "$count" -gt "$BASELINE" ]; then
|
||||||
|
grep -rnE --include='*.rs' \
|
||||||
|
'&[A-Za-z_][A-Za-z0-9_]*\[\.\.[A-Za-z0-9_]+\]' \
|
||||||
|
crates/apps/nigig-sms/src || true
|
||||||
|
echo
|
||||||
|
echo "ERROR: $count byte-offset slice(s), up from $BASELINE."
|
||||||
|
echo "These index a &str by BYTE offset and panic when the"
|
||||||
|
echo "offset is not a char boundary -- reachable from any"
|
||||||
|
echo "inbound SMS containing emoji or non-Latin text. Use"
|
||||||
|
echo "char_indices() to find a real boundary first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$count" -lt "$BASELINE" ]; then
|
||||||
|
echo
|
||||||
|
echo "Good: down to $count. Lower BASELINE in this file to $count."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# robius-sms: the platform layer. Cheap -- no GUI stack.
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
robius-sms:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# polkit/gio are pulled in ONLY by the Linux backend, whose every
|
||||||
|
# function returns PermanentlyUnavailable. They are why
|
||||||
|
# deny-nigig-build.toml carries RUSTSEC-2024-0370 and
|
||||||
|
# RUSTSEC-2024-0429 plus an open LGPL-2.1 question. Phase E9 of the
|
||||||
|
# remediation plan removes the backend and these packages with it;
|
||||||
|
# until then CI needs the headers to build the stub.
|
||||||
|
- name: Install native dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq \
|
||||||
|
pkg-config libpolkit-gobject-1-dev libpolkit-agent-1-dev \
|
||||||
|
libglib2.0-dev libsqlite3-dev
|
||||||
|
|
||||||
|
- name: Host check
|
||||||
|
run: cargo check --locked -p robius-sms
|
||||||
|
|
||||||
|
- name: Host clippy
|
||||||
|
run: cargo clippy --locked -p robius-sms --all-targets -- -D warnings
|
||||||
|
|
||||||
|
- name: Host tests
|
||||||
|
run: cargo test --locked -p robius-sms
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# The Android target. This is the job that matters: everything in
|
||||||
|
# sys/android/ is invisible to every other job in this file.
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
android:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 40
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# d8 (build-tools 34) rejects class file major version > 61, i.e.
|
||||||
|
# anything newer than Java 17. A runner defaulting to JDK 21 makes
|
||||||
|
# build.rs fail inside d8 with "Unsupported class file major
|
||||||
|
# version 65", which reads like a toolchain bug rather than a JDK
|
||||||
|
# mismatch. Pin the JDK so that failure cannot recur here.
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
|
- name: Install Android SDK packages
|
||||||
|
run: sdkmanager "platforms;android-34" "build-tools;34.0.0"
|
||||||
|
|
||||||
|
- name: Add Rust Android target
|
||||||
|
run: rustup target add aarch64-linux-android
|
||||||
|
|
||||||
|
# robius-sms/src/build.rs compiles two .java files with javac and
|
||||||
|
# dexes them with d8, so this step exercises the Java toolchain and
|
||||||
|
# the runtime-dex-loading path, not just the Rust.
|
||||||
|
- name: Android check
|
||||||
|
run: cargo check --locked -p robius-sms --target aarch64-linux-android
|
||||||
|
|
||||||
|
- name: Android clippy
|
||||||
|
run: |
|
||||||
|
cargo clippy --locked -p robius-sms \
|
||||||
|
--target aarch64-linux-android -- -D warnings
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# nigig-sms: the UI layer. Pulls the whole Makepad stack, so it is the
|
||||||
|
# slow job and needs the GUI system libraries.
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
nigig-sms:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install native dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -qq
|
||||||
|
sudo apt-get install -y -qq \
|
||||||
|
pkg-config libwayland-dev libxcursor-dev libxrandr-dev \
|
||||||
|
libxi-dev libx11-dev libgl1-mesa-dev libasound2-dev \
|
||||||
|
libpolkit-gobject-1-dev libpolkit-agent-1-dev \
|
||||||
|
libglib2.0-dev libssl-dev libsqlite3-dev libudev-dev \
|
||||||
|
libpulse-dev libxkbcommon-dev
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
run: cargo check --locked -p nigig-sms
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --locked -p nigig-sms
|
||||||
|
|
||||||
|
# `-D warnings` cannot be applied to the whole `-p nigig-sms` build:
|
||||||
|
# it also compiles nigig-core, nigig-uikit and matrix_client, which
|
||||||
|
# carry ~89 pre-existing warnings that are not this workflow's to
|
||||||
|
# fix. Muting them with --no-deps does not work either -- they are
|
||||||
|
# workspace members, not registry deps, so clippy still reports
|
||||||
|
# them.
|
||||||
|
#
|
||||||
|
# So: take clippy's JSON, keep only diagnostics whose package id is
|
||||||
|
# nigig-sms, and count those. Dependency warnings stay visible in
|
||||||
|
# the log above but do not fail the build. When nigig-core and
|
||||||
|
# nigig-uikit are cleaned up, this can collapse to a plain
|
||||||
|
# `-- -D warnings`.
|
||||||
|
#
|
||||||
|
# RATCHET at 50. Every one is mechanical (unused imports, dead
|
||||||
|
# code, map->for_each, needless borrows) and none is a logic
|
||||||
|
# change, but `cargo clippy --fix` cannot apply them: the crate is
|
||||||
|
# built around Makepad's script_mod! proc macro and rustfix
|
||||||
|
# refuses to edit through it. They therefore have to be fixed by
|
||||||
|
# hand, which is Phase F work, not Phase 0.
|
||||||
|
#
|
||||||
|
# 34 of the 50 are dead-code reports for the duplicate contact
|
||||||
|
# subsystem in inbox/sms_screen.rs -- an entire second copy of the
|
||||||
|
# cache, its lookup helpers and try_load_contacts(), none of it
|
||||||
|
# reachable. Deleting that file's dead half clears most of this
|
||||||
|
# number in one commit.
|
||||||
|
- name: Clippy ratchet (nigig-sms-owned diagnostics only)
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
BASELINE=50
|
||||||
|
cargo clippy --locked -p nigig-sms --all-targets \
|
||||||
|
--message-format=json > /tmp/clippy-sms.json 2>/tmp/clippy-sms.err || true
|
||||||
|
# Surface the human-readable log for debugging.
|
||||||
|
cat /tmp/clippy-sms.err || true
|
||||||
|
BASELINE="$BASELINE" python3 - <<'PY'
|
||||||
|
import json, os, sys
|
||||||
|
baseline = int(os.environ['BASELINE'])
|
||||||
|
owned, seen = [], set()
|
||||||
|
with open('/tmp/clippy-sms.json') as fh:
|
||||||
|
for line in fh:
|
||||||
|
try:
|
||||||
|
m = json.loads(line)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if m.get('reason') != 'compiler-message':
|
||||||
|
continue
|
||||||
|
if 'nigig-sms' not in m.get('package_id', ''):
|
||||||
|
continue
|
||||||
|
msg = m['message']
|
||||||
|
if msg.get('level') not in ('warning', 'error'):
|
||||||
|
continue
|
||||||
|
# --all-targets compiles lib and lib-test, duplicating
|
||||||
|
# every diagnostic; dedupe on rendered text.
|
||||||
|
key = msg.get('rendered', '')
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
owned.append(msg)
|
||||||
|
n = len(owned)
|
||||||
|
print("found %d nigig-sms diagnostics, baseline %d" % (n, baseline))
|
||||||
|
if n > baseline:
|
||||||
|
for msg in owned:
|
||||||
|
sys.stdout.write(msg.get('rendered', ''))
|
||||||
|
print()
|
||||||
|
print("ERROR: %d diagnostics, up from %d." % (n, baseline))
|
||||||
|
sys.exit(1)
|
||||||
|
if n < baseline:
|
||||||
|
print()
|
||||||
|
print("Good: down to %d. Lower BASELINE in this file to %d "
|
||||||
|
"so the progress cannot be undone." % (n, n))
|
||||||
|
sys.exit(1)
|
||||||
|
print("OK")
|
||||||
|
PY
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# Supply chain. Mirrors the gates already in nigig-build.yml so the SMS
|
||||||
|
# crates cannot drift from the rest of the workspace.
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
supply-chain:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# A lockfile that changes during CI means the committed one was
|
||||||
|
# stale. This is only enforceable because Phase 0.1 committed the
|
||||||
|
# root Cargo.lock -- before that it was gitignored and every build
|
||||||
|
# silently re-resolved.
|
||||||
|
- name: Lockfile must be committed and current
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -f Cargo.lock || { echo "missing Cargo.lock at workspace root"; exit 1; }
|
||||||
|
cargo metadata --locked --format-version 1 > /dev/null
|
||||||
|
git diff --exit-code -- Cargo.lock
|
||||||
|
|
||||||
|
- name: Reject whitespace errors
|
||||||
|
run: git diff --check
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
|
|
@ -1,5 +1,11 @@
|
||||||
target
|
target
|
||||||
Cargo.lock
|
# Phase 0.1: the workspace root lockfile is TRACKED. CI runs with --locked
|
||||||
|
# (see .forgejo/workflows/*.yml), which cannot work against an ignored
|
||||||
|
# lockfile: `cargo metadata --locked` fails outright and every build
|
||||||
|
# re-resolves, so a dependency can change under CI without any commit.
|
||||||
|
# Nested/vendored lockfiles stay ignored via the pattern below.
|
||||||
|
**/Cargo.lock
|
||||||
|
!/Cargo.lock
|
||||||
# Review item 1.1: the payment crates are validated standalone against a
|
# Review item 1.1: the payment crates are validated standalone against a
|
||||||
# checked-in lockfile, so their locks must be tracked. Application crates
|
# checked-in lockfile, so their locks must be tracked. Application crates
|
||||||
# keep using the ignore rule above.
|
# keep using the ignore rule above.
|
||||||
|
|
|
||||||
5487
Cargo.lock
generated
Normal file
5487
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -29,11 +29,9 @@ pub extern "C" fn Java_robius_sms_SmsBootReceiver_rustRestoreSchedules<'a>(
|
||||||
_: JObject<'a>,
|
_: JObject<'a>,
|
||||||
context: JObject<'a>,
|
context: JObject<'a>,
|
||||||
) {
|
) {
|
||||||
unsafe {
|
|
||||||
let mut env = env;
|
let mut env = env;
|
||||||
let _ = super::schedule::restore_all_schedules(&mut env, &context);
|
let _ = super::schedule::restore_all_schedules(&mut env, &context);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn ensure_loaded(env: &mut JNIEnv<'_>) -> Result<()> {
|
pub(crate) fn ensure_loaded(env: &mut JNIEnv<'_>) -> Result<()> {
|
||||||
if ALARM_RECEIVER_CLASS.get().is_some() && BOOT_RECEIVER_CLASS.get().is_some() {
|
if ALARM_RECEIVER_CLASS.get().is_some() && BOOT_RECEIVER_CLASS.get().is_some() {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ pub(crate) fn cancel_scheduled_sms(id: i32) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn list_scheduled_sms() -> Result<Vec<ScheduledMessage>> {
|
pub(crate) fn list_scheduled_sms() -> Result<Vec<ScheduledMessage>> {
|
||||||
robius_android_env::with_activity(|env, activity| load_schedules(env, activity))
|
robius_android_env::with_activity(load_schedules)
|
||||||
.map_err(|_| Error::AndroidEnvironment)
|
.map_err(|_| Error::AndroidEnvironment)
|
||||||
.and_then(|x| x)
|
.and_then(|x| x)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use jni::{
|
||||||
use crate::{Error, Result, SmsThread};
|
use crate::{Error, Result, SmsThread};
|
||||||
|
|
||||||
pub(crate) fn list_threads() -> Result<Vec<SmsThread>> {
|
pub(crate) fn list_threads() -> Result<Vec<SmsThread>> {
|
||||||
robius_android_env::with_activity(|env, activity| list_threads_inner(env, activity))
|
robius_android_env::with_activity(list_threads_inner)
|
||||||
.map_err(|_| Error::AndroidEnvironment)
|
.map_err(|_| Error::AndroidEnvironment)
|
||||||
.and_then(|x| x)
|
.and_then(|x| x)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue