commit ca30868e90ad52e27e3ada236d2eb4ec68b3807f
parent 2bf0680eb18353f242769b5906e90e3553f9c40b
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 00:05:01 +0000
ios: make offline geocoder staging idempotent
- version the staged ios geocoder path from the bundled asset metadata instead of rewriting one fixed file
- reuse the current staged geocoder database when it already exists for the active bundled asset revision
- narrow the ios crate unsafe allowance to the clipboard ffi and exported entrypoint sites that require it
- add an ios regression test that proves existing staged geocoder copies are not overwritten on startup
Diffstat:
3 files changed, 67 insertions(+), 12 deletions(-)
diff --git a/crates/ios/Cargo.toml b/crates/ios/Cargo.toml
@@ -25,6 +25,3 @@ zeroize.workspace = true
[target.'cfg(target_os = "ios")'.dependencies]
wgpu = { workspace = true, features = ["metal", "wgsl"] }
-
-[lints.rust]
-unsafe_code = { level = "allow", priority = 1 }
diff --git a/crates/ios/src/lib.rs b/crates/ios/src/lib.rs
@@ -1,5 +1,3 @@
-#![allow(unsafe_code)]
-
#[cfg(target_os = "ios")]
use eframe::egui::ViewportBuilder;
#[cfg(target_os = "ios")]
@@ -35,6 +33,7 @@ struct IosBackend {
}
#[cfg(target_os = "ios")]
+#[allow(unsafe_code)]
unsafe extern "C" {
fn radroots_ios_clipboard_text_copy() -> *mut std::ffi::c_char;
fn radroots_ios_string_free(value: *mut std::ffi::c_char);
@@ -159,6 +158,7 @@ impl IosBackend {
}
#[cfg(target_os = "ios")]
+ #[allow(unsafe_code)]
fn paste_secret_key_from_clipboard() -> Result<String, String> {
let clipboard_text_ptr = unsafe { radroots_ios_clipboard_text_copy() };
if clipboard_text_ptr.is_null() {
@@ -346,6 +346,7 @@ pub fn run() -> Result<(), String> {
pub const ENTRYPOINT_SYMBOL: &str = "radroots_ios_run";
+#[allow(unsafe_code)]
#[unsafe(no_mangle)]
pub extern "C" fn radroots_ios_run() -> i32 {
match run() {
diff --git a/crates/ios/src/offline_geocoder.rs b/crates/ios/src/offline_geocoder.rs
@@ -7,6 +7,7 @@ use radroots_geocoder::Geocoder;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
+use std::time::UNIX_EPOCH;
const GEOCODER_ASSET_FILENAME: &str = "geonames.db";
@@ -86,7 +87,7 @@ fn initialize_offline_geocoder_inner(
));
}
- let staged_path = staged_db_path(app_data_root);
+ let staged_path = staged_db_path(app_data_root, asset_revision(source_path.as_path())?);
stage_bundled_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| {
(
RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
@@ -112,24 +113,55 @@ fn bundled_asset_path() -> Result<PathBuf, String> {
Ok(parent.join(GEOCODER_ASSET_FILENAME))
}
-fn staged_db_path(app_data_root: &Path) -> PathBuf {
- app_data_root.join("geocoder").join(GEOCODER_ASSET_FILENAME)
+fn asset_revision(
+ source_path: &Path,
+) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> {
+ let metadata = std::fs::metadata(source_path).map_err(|source| {
+ (
+ RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
+ format!("failed to read ios geocoder asset metadata: {source}"),
+ )
+ })?;
+ let modified = metadata.modified().map_err(|source| {
+ (
+ RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
+ format!("failed to read ios geocoder asset mtime: {source}"),
+ )
+ })?;
+ let modified = modified.duration_since(UNIX_EPOCH).map_err(|source| {
+ (
+ RadrootsOfflineGeocoderUnavailableKind::InitializationFailed,
+ format!("failed to normalize ios geocoder asset mtime: {source}"),
+ )
+ })?;
+ Ok(format!("{:x}-{:x}", metadata.len(), modified.as_secs()))
}
-fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<(), String> {
+fn staged_db_path(app_data_root: &Path, revision: String) -> PathBuf {
+ app_data_root
+ .join("geocoder")
+ .join(revision)
+ .join(GEOCODER_ASSET_FILENAME)
+}
+
+fn stage_bundled_asset(source_path: &Path, staged_path: &Path) -> Result<bool, String> {
let Some(parent) = staged_path.parent() else {
return Err("staged ios geocoder path did not have a parent directory".to_owned());
};
std::fs::create_dir_all(parent)
.map_err(|source| format!("failed to create ios geocoder directory: {source}"))?;
+ if staged_path.is_file() {
+ return Ok(false);
+ }
std::fs::copy(source_path, staged_path)
.map_err(|source| format!("failed to stage ios geocoder asset: {source}"))?;
- Ok(())
+ Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
+ use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn staged_db_path_uses_ios_geocoder_directory() {
@@ -138,9 +170,9 @@ mod tests {
);
assert_eq!(
- staged_db_path(app_data_root.as_path()),
+ staged_db_path(app_data_root.as_path(), "abcd".to_owned()),
PathBuf::from(
- "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/app/ios/geocoder/geonames.db"
+ "/var/mobile/Containers/Data/Application/example/Library/Application Support/RadRoots/app/ios/geocoder/abcd/geonames.db"
)
);
}
@@ -161,4 +193,29 @@ mod tests {
}
);
}
+
+ #[test]
+ fn stage_bundled_asset_reuses_existing_staged_copy() {
+ let temp_root = std::env::temp_dir().join(format!(
+ "radroots-ios-geocoder-test-{}",
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ ));
+ let source_path = temp_root.join("source.db");
+ let staged_path = temp_root.join("staged").join("geonames.db");
+
+ std::fs::create_dir_all(temp_root.as_path()).unwrap();
+ std::fs::write(source_path.as_path(), b"source").unwrap();
+ std::fs::create_dir_all(staged_path.parent().unwrap()).unwrap();
+ std::fs::write(staged_path.as_path(), b"existing").unwrap();
+
+ let copied = stage_bundled_asset(source_path.as_path(), staged_path.as_path()).unwrap();
+
+ assert!(!copied);
+ assert_eq!(std::fs::read(staged_path.as_path()).unwrap(), b"existing");
+
+ std::fs::remove_dir_all(temp_root.as_path()).unwrap();
+ }
}