use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
use crate::{Error, Result};
#[cfg(target_os = "macos")]
const EXTENSION_FILENAME: &str = "graphqlite.dylib";
#[cfg(target_os = "linux")]
const EXTENSION_FILENAME: &str = "graphqlite.so";
#[cfg(target_os = "windows")]
const EXTENSION_FILENAME: &str = "graphqlite.dll";
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
const EXTENSION_BYTES: &[u8] = include_bytes!("../libs/graphqlite-macos-x86_64.dylib");
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
const EXTENSION_BYTES: &[u8] = include_bytes!("../libs/graphqlite-macos-aarch64.dylib");
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const EXTENSION_BYTES: &[u8] = include_bytes!("../libs/graphqlite-linux-x86_64.so");
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
const EXTENSION_BYTES: &[u8] = include_bytes!("../libs/graphqlite-linux-aarch64.so");
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
const EXTENSION_BYTES: &[u8] = include_bytes!("../libs/graphqlite-windows-x86_64.dll");
static EXTENSION_PATH: Mutex<Option<PathBuf>> = Mutex::new(None);
pub fn get_extension_path() -> Result<PathBuf> {
let mut cached = EXTENSION_PATH.lock().map_err(|e| {
Error::ExtensionNotFound(format!("Failed to acquire lock: {}", e))
})?;
if let Some(ref path) = *cached {
return Ok(path.clone());
}
let path = extract_extension()?;
*cached = Some(path.clone());
Ok(path)
}
fn extract_extension() -> Result<PathBuf> {
let version = env!("CARGO_PKG_VERSION");
let filename = format!("graphqlite-{}-{}", version, EXTENSION_FILENAME);
if let Ok(custom_dir) = std::env::var("GRAPHQLITE_EXTENSION_DIR") {
let dir = PathBuf::from(custom_dir);
if let Ok(path) = try_extract_to(&dir, &filename) {
cleanup_old_versions(&dir, &filename);
return Ok(path);
}
}
let dirs_to_try = [
std::env::temp_dir().join("graphqlite"),
dirs_fallback(),
];
for dir in &dirs_to_try {
if let Ok(path) = try_extract_to(dir, &filename) {
cleanup_old_versions(dir, &filename);
return Ok(path);
}
}
Err(Error::ExtensionNotFound(
"Failed to extract extension to temp dir or ~/.cache/graphqlite/. \
If /tmp has noexec, set GRAPHQLITE_EXTENSION_DIR to a writable, executable path."
.to_string(),
))
}
fn dirs_fallback() -> PathBuf {
#[cfg(unix)]
{
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".cache").join("graphqlite");
}
}
#[cfg(windows)]
{
if let Ok(appdata) = std::env::var("LOCALAPPDATA") {
return PathBuf::from(appdata).join("graphqlite");
}
}
PathBuf::from(".graphqlite-cache")
}
fn try_extract_to(dir: &PathBuf, filename: &str) -> Result<PathBuf> {
std::fs::create_dir_all(dir).map_err(|e| {
Error::ExtensionNotFound(format!("Failed to create directory {:?}: {}", dir, e))
})?;
let extension_path = dir.join(filename);
let needs_extract = match std::fs::metadata(&extension_path) {
Ok(meta) => meta.len() != EXTENSION_BYTES.len() as u64,
Err(_) => true,
};
if needs_extract {
let tmp_path = extension_path.with_extension("tmp");
let mut file = std::fs::File::create(&tmp_path).map_err(|e| {
Error::ExtensionNotFound(format!("Failed to create temp file: {}", e))
})?;
file.write_all(EXTENSION_BYTES).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
Error::ExtensionNotFound(format!("Failed to write extension file: {}", e))
})?;
drop(file);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o755);
std::fs::set_permissions(&tmp_path, perms)
.map_err(|e| Error::ExtensionNotFound(format!("Failed to set permissions: {}", e)))?;
}
std::fs::rename(&tmp_path, &extension_path).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
Error::ExtensionNotFound(format!("Failed to rename extension into place: {}", e))
})?;
}
Ok(extension_path)
}
fn cleanup_old_versions(dir: &PathBuf, current_filename: &str) {
let prefix = "graphqlite-";
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(prefix) && name_str != current_filename && !name_str.ends_with(".tmp") {
let _ = std::fs::remove_file(entry.path());
}
}
}
}
pub fn load_bundled_extension(conn: &rusqlite::Connection) -> Result<()> {
let extension_path = get_extension_path()?;
let load_path = extension_path.with_extension("");
unsafe {
conn.load_extension_enable()?;
conn.load_extension(&load_path, None::<&str>)?;
conn.load_extension_disable()?;
}
let test: String = conn.query_row("SELECT graphqlite_test()", [], |row| row.get(0))?;
if !test.to_lowercase().contains("successfully") {
return Err(Error::ExtensionNotFound(
"Extension loaded but verification failed".to_string(),
));
}
Ok(())
}