use std::path::{Path, PathBuf};
use crate::{FalkorDBError, FalkorResult};
pub const BUNDLED_MODULE: &[u8] = include_bytes!(env!("FALKORDB_EMBEDDED_MODULE_FILE"));
pub const BUNDLED_MODULE_VERSION: &str = env!("FALKORDB_EMBEDDED_MODULE_VERSION");
pub const BUNDLED_MODULE_PLATFORM: &str = env!("FALKORDB_EMBEDDED_MODULE_PLATFORM");
pub const BUNDLED_MODULE_SHA256: &str = env!("FALKORDB_EMBEDDED_MODULE_SHA256");
pub fn materialize_bundled_module(cache_dir: Option<&Path>) -> FalkorResult<PathBuf> {
let dir = cache_root(cache_dir)
.join("bundled")
.join(BUNDLED_MODULE_SHA256);
let dest = dir.join("falkordb.so");
if extracted_matches(&dest) {
return Ok(dest);
}
create_private_dir(&dir)?;
write_atomic(&dir, &dest)?;
Ok(dest)
}
fn extracted_matches(dest: &Path) -> bool {
file_len(dest) == Some(BUNDLED_MODULE.len() as u64)
&& std::fs::read(dest).is_ok_and(|bytes| bytes == BUNDLED_MODULE)
}
fn create_private_dir(dir: &Path) -> FalkorResult<()> {
std::fs::create_dir_all(dir).map_err(|e| {
FalkorDBError::EmbeddedServerError(format!(
"Failed to create bundled-module cache dir {}: {e}",
dir.display()
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
FalkorDBError::EmbeddedServerError(format!(
"Failed to secure bundled-module cache dir {} with owner-only permissions: {e}",
dir.display()
))
})?;
}
Ok(())
}
fn file_len(path: &Path) -> Option<u64> {
std::fs::metadata(path).ok().map(|m| m.len())
}
fn write_atomic(
dir: &Path,
dest: &Path,
) -> FalkorResult<()> {
use std::io::Write;
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let unique = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let tmp = dir.join(format!("falkordb.so.tmp.{}.{unique}", std::process::id()));
let write_result = (|| -> std::io::Result<()> {
let mut file = std::fs::File::create(&tmp)?;
file.write_all(BUNDLED_MODULE)?;
file.flush()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
file.set_permissions(std::fs::Permissions::from_mode(0o644))?;
}
Ok(())
})();
if let Err(e) = write_result {
let _ = std::fs::remove_file(&tmp);
return Err(FalkorDBError::EmbeddedServerError(format!(
"Failed to extract bundled FalkorDB module to {}: {e}",
tmp.display()
)));
}
match std::fs::rename(&tmp, dest) {
Ok(()) => Ok(()),
Err(rename_err) => {
let _ = std::fs::remove_file(&tmp);
if extracted_matches(dest) {
Ok(())
} else {
Err(FalkorDBError::EmbeddedServerError(format!(
"Failed to finalize bundled FalkorDB module at {}: {rename_err}",
dest.display()
)))
}
}
}
}
fn cache_root(cache_dir: Option<&Path>) -> PathBuf {
if let Some(dir) = cache_dir {
return dir.to_path_buf();
}
if let Some(dir) = std::env::var_os("FALKORDB_RS_CACHE_DIR") {
return PathBuf::from(dir);
}
if let Some(home) = std::env::var_os("HOME") {
let home = PathBuf::from(home);
#[cfg(target_os = "macos")]
let base = home.join("Library").join("Caches");
#[cfg(not(target_os = "macos"))]
let base = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".cache"));
return base.join("falkordb-rs");
}
std::env::temp_dir().join("falkordb-rs")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bundled_metadata_is_present() {
assert!(!BUNDLED_MODULE.is_empty());
assert!(BUNDLED_MODULE_VERSION.starts_with('v'));
assert!(!BUNDLED_MODULE_PLATFORM.is_empty());
assert_eq!(BUNDLED_MODULE_SHA256.len(), 64);
assert!(BUNDLED_MODULE_SHA256.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_materialize_extracts_and_reuses() {
let tmp = std::env::temp_dir().join(format!("falkordb-bundle-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
let first = materialize_bundled_module(Some(&tmp)).expect("extract");
assert!(first.exists());
assert_eq!(
std::fs::metadata(&first).unwrap().len(),
BUNDLED_MODULE.len() as u64
);
assert_eq!(std::fs::read(&first).unwrap(), BUNDLED_MODULE);
let second = materialize_bundled_module(Some(&tmp)).expect("reuse");
assert_eq!(first, second);
std::fs::write(&first, b"corrupt").unwrap();
let third = materialize_bundled_module(Some(&tmp)).expect("repair");
assert_eq!(third, first);
assert_eq!(std::fs::read(&third).unwrap(), BUNDLED_MODULE);
std::fs::write(&first, vec![0u8; BUNDLED_MODULE.len()]).unwrap();
let repaired = materialize_bundled_module(Some(&tmp)).expect("re-extract on tamper");
assert_eq!(std::fs::read(&repaired).unwrap(), BUNDLED_MODULE);
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn test_cache_root_prefers_explicit() {
let explicit = PathBuf::from("/tmp/explicit-cache");
assert_eq!(cache_root(Some(&explicit)), explicit);
}
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct EnvVarGuard {
key: &'static str,
original: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn capture(key: &'static str) -> Self {
Self {
key,
original: std::env::var_os(key),
}
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
#[test]
fn test_cache_root_env_and_home_fallbacks() {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _cache = EnvVarGuard::capture("FALKORDB_RS_CACHE_DIR");
let _home = EnvVarGuard::capture("HOME");
let _xdg = EnvVarGuard::capture("XDG_CACHE_HOME");
std::env::set_var("FALKORDB_RS_CACHE_DIR", "/custom/cache");
assert_eq!(cache_root(None), PathBuf::from("/custom/cache"));
std::env::remove_var("FALKORDB_RS_CACHE_DIR");
std::env::remove_var("XDG_CACHE_HOME");
std::env::set_var("HOME", "/test/home");
let path = cache_root(None);
assert!(
path.to_string_lossy().contains("falkordb-rs"),
"got {path:?}"
);
assert!(path.starts_with("/test/home"), "got {path:?}");
std::env::remove_var("HOME");
let path = cache_root(None);
assert!(path.ends_with("falkordb-rs"), "got {path:?}");
}
#[test]
fn test_materialize_errors_when_cache_path_is_a_file() {
let file =
std::env::temp_dir().join(format!("falkordb-bundle-file-{}", std::process::id()));
std::fs::write(&file, b"not a dir").unwrap();
let err = materialize_bundled_module(Some(&file))
.expect_err("materialize should fail when the cache path is a file");
assert!(
err.to_string().contains("cache dir") || err.to_string().contains("Failed"),
"unexpected error: {err}"
);
let _ = std::fs::remove_file(&file);
}
}