refactor(android): forbid unsafe workspace-wide, quarantine JNI in app (#91)
Addresses #91: the #90 posture (`deny(unsafe_code)` + three scattered `#![allow(unsafe_code)]` across solitaire_data and solitaire_engine) punched unsafe holes into otherwise-pure logic crates. Replace it by *reducing* the unsafe rather than relocating it, then forbidding it everywhere it can be forbidden. Changes: - solitaire_data: new safe `android_jni` bridge owning the cached `JavaVM` and activity `GlobalRef`; exposes `with_env` / `with_activity_env` so keystore/clipboard/safe-area never touch a raw handle. - keystore: drop the `JavaVM::from_raw` init path (now in the app) and replace the three `unsafe { JByteArray::from_raw(x.into_raw()) }` casts with the safe `JByteArray::from(JObject)` conversion jni 0.21 provides. - engine clipboard + safe_area: route through the bridge; remove their `#![allow(unsafe_code)]` and all `from_raw` calls. - solitaire_app: becomes the single owner of FFI unsafe. `android_main` reconstructs the raw `JavaVM` / activity once (it must, as the cdylib that exports `#[unsafe(no_mangle)]`) and registers the safe wrappers. It opts to its own `deny`-level lints with two scoped `#[allow(unsafe_code)]`. - workspace: `unsafe_code` is now `forbid`. Every crate except the app entry point is fully unsafe-free. Net: 7 unsafe sites across three crates collapse to 3 at the OS boundary in one crate. Verified with host `clippy --workspace -- -D warnings` and an `aarch64-linux-android` clippy build of solitaire_app (transitively engine + data); also fixed two latent android-only `collapsible_if` warnings surfaced in the keystore by the cross-target check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,13 @@ bevy = { workspace = true }
|
||||
solitaire_engine = { workspace = true }
|
||||
solitaire_data = { workspace = true }
|
||||
|
||||
# Android-only: the entry point reconstructs the raw `JavaVM` / activity
|
||||
# handles and registers the safe `solitaire_data::android_jni` bridge. This
|
||||
# is the one crate in the workspace that performs `unsafe` FFI, so it is also
|
||||
# the only one that depends on `jni` directly at the app layer.
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
jni = { workspace = true }
|
||||
|
||||
# Desktop-only deps. `keyring`'s default-store init only matters on
|
||||
# platforms with a real keychain backend (Linux Secret Service,
|
||||
# macOS Keychain, Windows Credential Store), and its transitive
|
||||
@@ -100,5 +107,17 @@ icon = "@mipmap/ic_launcher"
|
||||
# enabling auto-rotate.
|
||||
orientation = "portrait"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# `solitaire_app` is the one crate that cannot inherit the workspace
|
||||
# `forbid(unsafe_code)`: as the Android cdylib it must export the
|
||||
# `#[unsafe(no_mangle)]` entry point and reconstruct the raw JNI handles
|
||||
# there (a `no_mangle` symbol cannot live in a dependency rlib). It mirrors
|
||||
# the workspace lints but at `deny`, so the two `#[allow(unsafe_code)]`
|
||||
# scopes in the Android entry point are the only unsafe in the whole tree.
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
single_use_lifetimes = "warn"
|
||||
trivial_casts = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_qualifications = "warn"
|
||||
variant_size_differences = "warn"
|
||||
unexpected_cfgs = "warn"
|
||||
|
||||
Reference in New Issue
Block a user