app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 6a18f9c2ffe5dab3dfce87a295b41c76ff01091b
parent 755a26c512c77ff1436a857b51127a8477dc2028
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 00:55:46 +0000

desktop: use stamped offline geocoder revisions

- package the stamped geocoder revision sidecar alongside the optional desktop geocoder db asset
- load the desktop offline geocoder revision from the bundled sidecar instead of file metadata heuristics
- treat an invalid or missing bundled revision sidecar as a build-asset unavailability state at runtime
- extend desktop tests to cover the stamped revision parser in addition to the existing staging and prune flows

Diffstat:
Mcrates/desktop/build.rs | 54++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/desktop/src/offline_geocoder.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 102 insertions(+), 32 deletions(-)

diff --git a/crates/desktop/build.rs b/crates/desktop/build.rs @@ -2,9 +2,12 @@ use std::env; use std::path::{Path, PathBuf}; use std::process::Command; +const GEOCODER_DB_FILENAME: &str = "geonames.db"; +const GEOCODER_REVISION_FILENAME: &str = "geonames.revision"; + fn main() { println!("cargo:rerun-if-changed=build.rs"); - sync_optional_geocoder_asset(); + sync_optional_geocoder_assets(); if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() != Some("macos") { return; @@ -47,30 +50,57 @@ fn main() { ); } -fn sync_optional_geocoder_asset() { +fn sync_optional_geocoder_assets() { let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir")); - let source_path = manifest_dir.join("../../assets/geocoder/geonames.db"); - println!("cargo:rerun-if-changed={}", source_path.display()); + let source_db_path = manifest_dir.join(format!("../../assets/geocoder/{GEOCODER_DB_FILENAME}")); + let source_revision_path = manifest_dir.join(format!( + "../../assets/geocoder/{GEOCODER_REVISION_FILENAME}" + )); + println!("cargo:rerun-if-changed={}", source_db_path.display()); + println!("cargo:rerun-if-changed={}", source_revision_path.display()); let profile_dir = target_profile_dir(); - let target_path = profile_dir.join("geonames.db"); + let target_db_path = profile_dir.join(GEOCODER_DB_FILENAME); + let target_revision_path = profile_dir.join(GEOCODER_REVISION_FILENAME); + + if source_db_path.is_file() { + if !source_revision_path.is_file() { + panic!( + "stamped desktop geocoder revision asset missing at {}", + source_revision_path.display() + ); + } - if source_path.is_file() { - std::fs::copy(&source_path, &target_path).unwrap_or_else(|err| { + std::fs::copy(&source_db_path, &target_db_path).unwrap_or_else(|err| { panic!( "failed to copy optional desktop geocoder asset from {} to {}: {err}", - source_path.display(), - target_path.display() + source_db_path.display(), + target_db_path.display() + ) + }); + std::fs::copy(&source_revision_path, &target_revision_path).unwrap_or_else(|err| { + panic!( + "failed to copy optional desktop geocoder revision from {} to {}: {err}", + source_revision_path.display(), + target_revision_path.display() ) }); return; } - if target_path.exists() { - std::fs::remove_file(&target_path).unwrap_or_else(|err| { + if target_db_path.exists() { + std::fs::remove_file(&target_db_path).unwrap_or_else(|err| { panic!( "failed to remove stale desktop geocoder asset at {}: {err}", - target_path.display() + target_db_path.display() + ) + }); + } + if target_revision_path.exists() { + std::fs::remove_file(&target_revision_path).unwrap_or_else(|err| { + panic!( + "failed to remove stale desktop geocoder revision at {}: {err}", + target_revision_path.display() ) }); } diff --git a/crates/desktop/src/offline_geocoder.rs b/crates/desktop/src/offline_geocoder.rs @@ -3,9 +3,9 @@ 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"; +const GEOCODER_REVISION_FILENAME: &str = "geonames.revision"; #[derive(Clone)] pub(crate) struct DesktopOfflineGeocoder { @@ -86,7 +86,7 @@ fn initialize_offline_geocoder_inner( )); } - let revision = asset_revision(source_path.as_path())?; + let revision = runtime_asset_revision(source_path.parent().unwrap_or_else(|| Path::new(".")))?; let staged_path = staged_db_path(app_data_root, revision.as_str()); stage_runtime_asset(source_path.as_path(), staged_path.as_path()).map_err(|debug_message| { ( @@ -113,28 +113,34 @@ fn runtime_asset_path() -> Result<PathBuf, String> { Ok(parent.join(GEOCODER_ASSET_FILENAME)) } -fn asset_revision( - source_path: &Path, +fn runtime_asset_revision( + asset_dir: &Path, ) -> Result<String, (RadrootsOfflineGeocoderUnavailableKind, String)> { - let metadata = std::fs::metadata(source_path).map_err(|source| { + let revision_path = asset_dir.join(GEOCODER_REVISION_FILENAME); + let revision = std::fs::read_to_string(revision_path.as_path()).map_err(|source| { ( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - format!("failed to read desktop geocoder asset metadata: {source}"), - ) - })?; - let modified = metadata.modified().map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - format!("failed to read desktop geocoder asset mtime: {source}"), - ) - })?; - let modified = modified.duration_since(UNIX_EPOCH).map_err(|source| { - ( - RadrootsOfflineGeocoderUnavailableKind::InitializationFailed, - format!("failed to normalize desktop geocoder asset mtime: {source}"), + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + format!( + "desktop bundled geocoder revision asset missing at {}: {source}", + revision_path.display() + ), ) })?; - Ok(format!("{:x}-{:x}", metadata.len(), modified.as_secs())) + let revision = revision.trim(); + if !is_valid_revision(revision) { + return Err(( + RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, + format!( + "desktop bundled geocoder revision asset invalid at {}", + revision_path.display() + ), + )); + } + Ok(revision.to_owned()) +} + +fn is_valid_revision(revision: &str) -> bool { + revision.len() == 64 && revision.bytes().all(|byte| byte.is_ascii_hexdigit()) } fn staged_geocoder_root(app_data_root: &Path) -> PathBuf { @@ -219,6 +225,17 @@ mod tests { } #[test] + fn valid_revision_requires_sha256_hex() { + assert!(is_valid_revision( + "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c" + )); + assert!(!is_valid_revision("abcd")); + assert!(!is_valid_revision( + "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079z" + )); + } + + #[test] fn missing_asset_maps_to_build_unavailable_message() { let state = RadrootsOfflineGeocoderState::unavailable( RadrootsOfflineGeocoderUnavailableKind::MissingBuildAsset, @@ -288,4 +305,27 @@ mod tests { std::fs::remove_dir_all(temp_root.as_path()).unwrap(); } + + #[test] + fn runtime_asset_revision_reads_stamped_sidecar() { + let temp_root = std::env::temp_dir().join(format!( + "radroots-desktop-geocoder-revision-test-{}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let revision_path = temp_root.join(GEOCODER_REVISION_FILENAME); + let revision = "6ca5f1a324de02922d40b1ff33eedf3a5a133c978de921eee5130a0c7876079c"; + + std::fs::create_dir_all(temp_root.as_path()).unwrap(); + std::fs::write(revision_path.as_path(), format!("{revision}\n")).unwrap(); + + assert_eq!( + runtime_asset_revision(temp_root.as_path()).unwrap(), + revision.to_owned() + ); + + std::fs::remove_dir_all(temp_root.as_path()).unwrap(); + } }