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:
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();
+ }
}