use path_slash::PathBufExt;
use std::borrow::Cow;
use std::ffi::{OsStr, c_void};
use std::fmt::Write;
use std::fs;
use std::path::{Path, PathBuf};
use std::{env, ops};
use libloading::Symbol;
use toml::de::{DeTable, DeValue};
use adbc_core::{
LOAD_FLAG_ALLOW_RELATIVE_PATHS, LOAD_FLAG_SEARCH_ENV, LOAD_FLAG_SEARCH_SYSTEM,
LOAD_FLAG_SEARCH_USER, LoadFlags,
error::{Error, Result, Status},
options::AdbcVersion,
};
use adbc_ffi::{
options::check_status,
{FFI_AdbcDriver, FFI_AdbcDriverInitFunc, FFI_AdbcError},
};
use crate::error::libloading_error_to_adbc_error;
const ERR_DETAIL_DRIVER_LOAD_TRACE: &str = "adbc.drivermanager.driver_load_trace";
#[derive(Debug, Default)]
struct DriverInfo {
lib_path: PathBuf,
entrypoint: Option<Vec<u8>>,
}
impl DriverInfo {
fn load_driver_manifest(manifest_file: &Path) -> Result<DriverInfo> {
let contents = fs::read_to_string(manifest_file).map_err(|e| {
Error::with_message_and_status(
format!("Could not read manifest '{}': {e}", manifest_file.display()),
Status::InvalidArguments,
)
})?;
let manifest = DeTable::parse(&contents)
.map_err(|e| Error::with_message_and_status(e.to_string(), Status::InvalidArguments))?;
let manifest_version = manifest
.get_ref()
.get("manifest_version")
.and_then(|v| v.get_ref().as_integer())
.map(|v| v.as_str())
.unwrap_or("1");
if manifest_version != "1" {
return Err(Error::with_message_and_status(
format!("Unsupported manifest version: {manifest_version}"),
Status::InvalidArguments,
));
}
let (os, arch, extra) = arch_triplet();
let driver_spanned = manifest.get_ref().get("Driver");
let lib_path = match driver_spanned
.and_then(|v| v.get_ref().get("shared"))
.map(|v| v.get_ref())
{
Some(DeValue::String(path)) => Ok(PathBuf::from(path.as_ref())),
Some(DeValue::Table(driver)) => {
let key = format!("{os}_{arch}{extra}");
let path = driver
.get(key.as_str())
.and_then(|v| v.get_ref().as_str())
.unwrap_or_default();
Ok(PathBuf::from(path))
}
Some(driver) => Err(Error::with_message_and_status(
format!(
"Driver.shared must be a string or a table, found '{}'",
driver.type_str()
),
Status::InvalidArguments,
)),
None => Err(Error::with_message_and_status(
format!(
"Manifest '{}' missing Driver.shared key",
manifest_file.display()
),
Status::InvalidArguments,
)),
}?;
if lib_path.as_os_str().is_empty() {
return Err(Error::with_message_and_status(
format!(
"Manifest '{}' found empty string for library path of Driver.shared.{os}_{arch}{extra}",
lib_path.display()
),
Status::InvalidArguments,
));
}
let entrypoint = driver_spanned
.and_then(|driver| driver.get_ref().as_table())
.and_then(|table| table.get("entrypoint"))
.map(|spanned| {
let val = spanned.get_ref();
match val.as_str() {
Some(s) => Ok(s.as_bytes().to_vec()),
None => Err(Error::with_message_and_status(
"Driver entrypoint must be a string".to_string(),
Status::InvalidArguments,
)),
}
})
.transpose()?;
Ok(DriverInfo {
lib_path,
entrypoint,
})
}
#[cfg(target_os = "windows")]
fn load_from_registry(root: &windows_registry::Key, driver_name: &OsStr) -> Result<DriverInfo> {
const ADBC_DRIVER_REGISTRY: &str = "SOFTWARE\\ADBC\\Drivers";
let drivers_key = root
.open(ADBC_DRIVER_REGISTRY)
.and_then(|k| k.open(driver_name.to_str().unwrap_or_default()))
.map_err(|e| {
Error::with_message_and_status(
format!("Failed to open registry key: {e}"),
Status::NotFound,
)
})?;
let manifest_version = drivers_key.get_u32("manifest_version").unwrap_or(1);
if manifest_version != 1 {
return Err(Error::with_message_and_status(
format!("Unsupported manifest version: {manifest_version}"),
Status::InvalidArguments,
));
}
let entrypoint = drivers_key
.get_string("entrypoint")
.ok()
.map(|s| s.into_bytes());
Ok(DriverInfo {
lib_path: PathBuf::from(drivers_key.get_string("driver").unwrap_or_default()),
entrypoint,
})
}
}
pub struct SearchHit {
pub lib_path: PathBuf,
pub library: libloading::Library,
pub entrypoint: Option<Vec<u8>>,
}
impl SearchHit {
pub fn new(
lib_path: PathBuf,
library: libloading::Library,
entrypoint: Option<Vec<u8>>,
) -> Self {
Self {
lib_path,
library,
entrypoint,
}
}
pub fn resolve_entrypoint<'a>(&'a self, entrypoint: Option<&'a [u8]>) -> Cow<'a, [u8]> {
if let Some(entrypoint) = self.entrypoint.as_deref() {
Cow::Borrowed(entrypoint)
} else {
DriverLibrary::derive_entrypoint(entrypoint, &self.lib_path)
}
}
}
enum DriverInitFunc<'a> {
Static(&'a FFI_AdbcDriverInitFunc),
Dynamic(Symbol<'a, FFI_AdbcDriverInitFunc>),
}
impl ops::Deref for DriverInitFunc<'_> {
type Target = FFI_AdbcDriverInitFunc;
fn deref(&self) -> &Self::Target {
match self {
DriverInitFunc::Static(init) => init,
DriverInitFunc::Dynamic(init) => init,
}
}
}
pub struct DriverLibrary<'a> {
init: DriverInitFunc<'a>,
}
impl<'a> DriverLibrary<'a> {
pub(crate) fn from_static_init(init: &'a FFI_AdbcDriverInitFunc) -> Self {
Self {
init: DriverInitFunc::Static(init),
}
}
pub fn try_from_dynamic_library(
library: &'a libloading::Library,
entrypoint: &[u8],
) -> Result<Self> {
let init: libloading::Symbol<adbc_ffi::FFI_AdbcDriverInitFunc> = unsafe {
library
.get(entrypoint)
.or_else(|_| library.get(b"AdbcDriverInit"))
.map_err(libloading_error_to_adbc_error)?
};
let init = DriverInitFunc::Dynamic(init);
Ok(Self { init })
}
pub fn init_driver(&self, version: AdbcVersion) -> Result<FFI_AdbcDriver> {
let mut error = FFI_AdbcError::default();
let mut driver = FFI_AdbcDriver::default();
let status = unsafe {
(self.init)(
version.into(),
&mut driver as *mut FFI_AdbcDriver as *mut c_void,
&mut error,
)
};
check_status(status, error)?;
Ok(driver)
}
pub fn load_library(filename: impl AsRef<OsStr>) -> Result<libloading::Library> {
#[cfg(unix)]
let library: libloading::Library = unsafe {
use std::os::raw::c_int;
const RTLD_NODELETE: c_int = if cfg!(any(
target_os = "macos",
target_os = "ios",
target_os = "tvos",
target_os = "visionos",
target_os = "watchos",
)) {
0x80
} else if cfg!(any(
target_os = "linux",
target_os = "android",
target_os = "emscripten",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "haiku",
target_os = "solaris",
target_os = "illumos",
target_env = "uclibc",
target_env = "newlib",
target_os = "fuchsia",
target_os = "redox",
target_os = "hurd",
target_os = "cygwin",
)) {
0x1000
} else {
0x0
};
libloading::os::unix::Library::open(
Some(filename.as_ref()),
libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_LOCAL | RTLD_NODELETE,
)
.map(Into::into)
.map_err(libloading_error_to_adbc_error)?
};
#[cfg(windows)]
let library: libloading::Library = unsafe {
let library: libloading::os::windows::Library =
libloading::os::windows::Library::new(filename.as_ref())
.map_err(libloading_error_to_adbc_error)?;
library.pin().map_err(libloading_error_to_adbc_error)?;
library.into()
};
Ok(library)
}
pub(crate) fn load_library_from_name(name: impl AsRef<str>) -> Result<libloading::Library> {
let filename = libloading::library_filename(name.as_ref());
Self::load_library(&filename)
}
fn load_library_from_manifest(manifest_file: &Path) -> Result<SearchHit> {
let info = DriverInfo::load_driver_manifest(manifest_file)?;
let library = Self::load_library(&info.lib_path)?;
Ok(SearchHit::new(info.lib_path, library, info.entrypoint))
}
pub(crate) fn derive_entrypoint(
entrypoint: Option<&[u8]>,
driver_path: impl AsRef<OsStr>,
) -> Cow<'_, [u8]> {
if let Some(entrypoint) = entrypoint {
Cow::Borrowed(entrypoint)
} else {
Cow::Owned(Self::get_default_entrypoint(driver_path).into_bytes())
}
}
pub(crate) fn derive_entrypoint_from_name<'b>(
entrypoint: Option<&'b [u8]>,
name: &str,
) -> Cow<'b, [u8]> {
if let Some(entrypoint) = entrypoint {
Cow::Borrowed(entrypoint)
} else {
Cow::Owned(Self::get_default_entrypoint_from_name(name).into_bytes())
}
}
pub fn search(
name: impl AsRef<OsStr>,
load_flags: LoadFlags,
additional_search_paths: Option<Vec<PathBuf>>,
) -> Result<SearchHit> {
let mut trace = Vec::new();
let driver_path = Path::new(name.as_ref());
let allow_relative = load_flags & LOAD_FLAG_ALLOW_RELATIVE_PATHS != 0;
let result = if let Some(ext) = driver_path.extension() {
if !allow_relative && driver_path.is_relative() {
Err(Error::with_message_and_status(
"Relative paths are not allowed",
Status::InvalidArguments,
))
} else if ext == "toml" {
DriverLibrary::load_library_from_manifest(driver_path)
} else {
let library = DriverLibrary::load_library(driver_path)?;
Ok(SearchHit::new(driver_path.to_path_buf(), library, None))
}
} else if driver_path.is_absolute() {
let toml_path = driver_path.with_extension("toml");
if toml_path.is_file() {
DriverLibrary::load_library_from_manifest(&toml_path)
} else {
let library = DriverLibrary::load_library(driver_path)?;
Ok(SearchHit::new(driver_path.to_path_buf(), library, None))
}
} else {
DriverLibrary::find_driver(driver_path, load_flags, additional_search_paths, &mut trace)
};
result.map_err(|mut err| {
if !trace.is_empty() {
let mut trace_text = String::new();
for (i, trace_entry) in trace.iter().enumerate() {
if i > 0 {
trace_text.push('\n');
}
unsafe { write!(&mut trace_text, "{trace_entry}").unwrap_unchecked() };
}
let mut details = err.details.take().unwrap_or_default();
details.push((
ERR_DETAIL_DRIVER_LOAD_TRACE.to_string(),
trace_text.into_bytes(),
));
err.details = Some(details);
}
err
})
}
#[cfg(target_os = "windows")]
fn load_library_from_registry(
root: &windows_registry::Key,
driver_name: &OsStr,
) -> Result<SearchHit> {
let info = DriverInfo::load_from_registry(root, driver_name)?;
let library = Self::load_library(&info.lib_path)?;
Ok(SearchHit::new(info.lib_path, library, info.entrypoint))
}
#[cfg(target_os = "windows")]
fn find_driver(
driver_path: &Path,
load_flags: LoadFlags,
additional_search_paths: Option<Vec<PathBuf>>,
trace: &mut Vec<Error>,
) -> Result<SearchHit> {
if load_flags & LOAD_FLAG_SEARCH_ENV != 0 {
match DriverLibrary::search_path_list(
driver_path,
&get_search_paths(LOAD_FLAG_SEARCH_ENV),
trace,
) {
Ok(result) => return Ok(result),
Err(err) => trace.push(err),
}
}
if let Some(additional_search_paths) = additional_search_paths {
match DriverLibrary::search_path_list(driver_path, &additional_search_paths, trace) {
Ok(result) => return Ok(result),
Err(err) => trace.push(err),
}
}
#[cfg(conda_build)]
if load_flags & LOAD_FLAG_SEARCH_ENV != 0 {
if let Some(conda_prefix) = env::var_os("CONDA_PREFIX") {
let conda_path = PathBuf::from(conda_prefix)
.join("etc")
.join("adbc")
.join("drivers");
match DriverLibrary::search_path_list(driver_path, &[conda_path], trace) {
Ok(result) => return Ok(result),
Err(err) => trace.push(err),
}
}
}
if load_flags & LOAD_FLAG_SEARCH_USER != 0 {
let result = DriverLibrary::load_library_from_registry(
windows_registry::CURRENT_USER,
driver_path.as_os_str(),
);
if let Err(err) = result {
trace.push(err);
} else {
return result;
}
match DriverLibrary::search_path_list(
driver_path,
&get_search_paths(LOAD_FLAG_SEARCH_USER),
trace,
) {
Ok(result) => return Ok(result),
Err(err) => trace.push(err),
}
}
if load_flags & LOAD_FLAG_SEARCH_SYSTEM != 0 {
let result = DriverLibrary::load_library_from_registry(
windows_registry::LOCAL_MACHINE,
driver_path.as_os_str(),
);
if let Err(err) = result {
trace.push(err);
} else {
return result;
}
match DriverLibrary::search_path_list(
driver_path,
&get_search_paths(LOAD_FLAG_SEARCH_SYSTEM),
trace,
) {
Ok(result) => return Ok(result),
Err(err) => trace.push(err),
}
}
let driver_name = driver_path.as_os_str().to_string_lossy().into_owned();
let library = DriverLibrary::load_library_from_name(driver_name)?;
Ok(SearchHit::new(driver_path.to_path_buf(), library, None))
}
#[cfg(not(windows))]
fn find_driver(
driver_path: &Path,
load_flags: LoadFlags,
additional_search_paths: Option<Vec<PathBuf>>,
trace: &mut Vec<Error>,
) -> Result<SearchHit> {
let mut path_list = get_search_paths(load_flags & LOAD_FLAG_SEARCH_ENV);
if let Some(additional_search_paths) = additional_search_paths {
path_list.extend(additional_search_paths);
}
#[cfg(conda_build)]
if load_flags & LOAD_FLAG_SEARCH_ENV != 0 {
if let Some(conda_prefix) = env::var_os("CONDA_PREFIX") {
let conda_path = PathBuf::from(conda_prefix)
.join("etc")
.join("adbc")
.join("drivers");
path_list.push(conda_path);
}
}
path_list.extend(get_search_paths(load_flags & !LOAD_FLAG_SEARCH_ENV));
match DriverLibrary::search_path_list(driver_path, &path_list, trace) {
Ok(hit) => return Ok(hit),
Err(err) => trace.push(err),
};
let driver_name = driver_path.as_os_str().to_string_lossy().into_owned();
match DriverLibrary::load_library_from_name(driver_name) {
Ok(library) => return Ok(SearchHit::new(driver_path.to_path_buf(), library, None)),
Err(err) => trace.push(err),
}
Err(Error::with_message_and_status(
format!("Driver not found: {}", driver_path.display()),
Status::NotFound,
))
}
fn search_path_list(
driver_path: &Path,
path_list: &[PathBuf],
trace: &mut Vec<Error>,
) -> Result<SearchHit> {
path_list
.iter()
.find_map(|path| {
let mut full_path = path.join(driver_path);
full_path.set_extension("toml");
if full_path.is_file() {
match DriverLibrary::load_library_from_manifest(&full_path) {
Ok(hit) => return Some(Ok(hit)),
Err(err) => trace.push(err),
}
}
full_path.set_extension(""); let library_path = match DriverLibrary::platform_library_path(&full_path) {
Ok(path) => path,
Err(err) => {
trace.push(err);
return None;
}
};
let result = DriverLibrary::load_library(&library_path)
.map(|library| SearchHit::new(library_path, library, None));
match result {
Ok(res) => return Some(Ok(res)),
Err(err) => trace.push(err),
}
None
})
.unwrap_or_else(|| {
Err(Error::with_message_and_status(
format!("Driver not found: {}", driver_path.display()),
Status::NotFound,
))
})
}
fn platform_library_path(path: &Path) -> Result<PathBuf> {
let file_name = path.file_name().ok_or_else(|| {
Error::with_message_and_status(
"Driver search path must include a file name",
Status::Internal,
)
})?;
Ok(path.with_file_name(libloading::library_filename(file_name)))
}
fn get_default_entrypoint(driver_path: impl AsRef<OsStr>) -> String {
let filename = driver_path.as_ref().to_str().unwrap_or_default();
let filename = filename
.rfind(['/', '\\'])
.map_or(filename, |pos| &filename[pos + 1..]);
let basename = filename
.find('.')
.map_or_else(|| filename, |pos| &filename[..pos]);
let name = basename
.strip_prefix(env::consts::DLL_PREFIX)
.unwrap_or(basename);
Self::get_default_entrypoint_from_name(name)
}
fn get_default_entrypoint_from_name(name: &str) -> String {
let entrypoint = name
.split(&['-', '_'][..])
.map(|s| {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect::<Vec<_>>()
.join("");
if entrypoint.starts_with("Adbc") {
format!("{entrypoint}Init")
} else {
format!("Adbc{entrypoint}Init")
}
}
}
const fn arch_triplet() -> (&'static str, &'static str, &'static str) {
#[cfg(target_arch = "x86_64")]
const ARCH: &str = "amd64";
#[cfg(all(target_arch = "aarch64", target_endian = "big"))]
const ARCH: &str = "arm64be";
#[cfg(all(target_arch = "aarch64", target_endian = "little"))]
const ARCH: &str = "arm64";
#[cfg(all(target_arch = "powerpc64", target_endian = "little"))]
const ARCH: &str = "powerpc64le";
#[cfg(all(target_arch = "powerpc64", target_endian = "big"))]
const ARCH: &str = "powerpc64";
#[cfg(target_arch = "riscv32")]
const ARCH: &str = "riscv";
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64",
target_arch = "riscv32",
)))]
const ARCH: &str = std::env::consts::ARCH;
const OS: &str = std::env::consts::OS;
#[cfg(target_env = "musl")]
const EXTRA: &str = "_musl";
#[cfg(all(target_os = "windows", target_env = "gnu"))]
const EXTRA: &str = "_mingw";
#[cfg(not(any(target_env = "musl", all(target_os = "windows", target_env = "gnu"))))]
const EXTRA: &str = "";
(OS, ARCH, EXTRA)
}
#[cfg(target_os = "windows")]
mod target_windows {
use windows_sys as windows;
use std::ffi::OsString;
use std::ffi::c_void;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::slice;
use windows::Win32::UI::Shell;
pub fn user_config_dir() -> Option<PathBuf> {
unsafe {
let mut path_ptr: windows::core::PWSTR = std::ptr::null_mut();
let result = Shell::SHGetKnownFolderPath(
&Shell::FOLDERID_LocalAppData,
0,
std::ptr::null_mut(),
&mut path_ptr,
);
if result == 0 {
let len = windows::Win32::Globalization::lstrlenW(path_ptr) as usize;
let path = slice::from_raw_parts(path_ptr, len);
let ostr: OsString = OsStringExt::from_wide(path);
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
Some(PathBuf::from(ostr))
} else {
windows::Win32::System::Com::CoTaskMemFree(path_ptr as *const c_void);
None
}
}
}
}
fn user_config_dir() -> Option<PathBuf> {
#[cfg(target_os = "windows")]
{
use target_windows::user_config_dir;
user_config_dir().map(|mut path| {
path.push("ADBC");
path.push("Drivers");
path
})
}
#[cfg(target_os = "macos")]
{
env::var_os("HOME").map(PathBuf::from).map(|mut path| {
path.push("Library");
path.push("Application Support");
path.push("ADBC");
path.push("Drivers");
path
})
}
#[cfg(all(unix, not(target_os = "macos")))]
{
env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| {
env::var_os("HOME").map(|home| {
let mut path = PathBuf::from(home);
path.push(".config");
path
})
})
.map(|mut path| {
path.push("adbc");
path.push("drivers");
path
})
}
}
fn system_config_dir() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
Some(PathBuf::from("/Library/Application Support/ADBC/Drivers"))
}
#[cfg(all(unix, not(target_os = "macos")))]
{
Some(PathBuf::from("/etc/adbc/drivers"))
}
#[cfg(not(unix))]
{
None
}
}
fn get_search_paths(lvls: LoadFlags) -> Vec<PathBuf> {
let mut result = Vec::new();
if lvls & LOAD_FLAG_SEARCH_ENV != 0 {
if let Some(paths) = env::var_os("ADBC_DRIVER_PATH") {
for p in env::split_paths(&paths) {
result.push(p);
}
}
}
if lvls & LOAD_FLAG_SEARCH_USER != 0 {
if let Some(path) = user_config_dir() {
if path.exists() {
result.push(path);
}
}
}
#[cfg(not(windows))]
if lvls & LOAD_FLAG_SEARCH_SYSTEM != 0 {
if let Some(path) = system_config_dir() {
if path.exists() {
result.push(path);
}
}
}
result
}
pub(crate) fn find_filesystem_profile(
name: &str,
additional_path_list: &Option<Vec<PathBuf>>,
) -> Result<PathBuf> {
let profile_path = PathBuf::from_slash(name);
let has_toml_ext = profile_path.extension().is_some_and(|ext| ext == "toml");
if profile_path.is_absolute() {
if has_toml_ext {
return Ok(profile_path);
}
return Ok(profile_path.with_extension("toml"));
}
if profile_path.is_file() {
return Ok(profile_path.to_path_buf());
}
let path_list = get_profile_search_paths(additional_path_list);
let actual_path = profile_path.with_extension("toml");
path_list
.iter()
.find_map(|path| {
let full_path = path.join(actual_path.as_path());
if full_path.is_file() {
Some(full_path)
} else {
None
}
})
.ok_or_else(|| {
Error::with_message_and_status(format!("Profile not found: {}", name), Status::NotFound)
})
}
fn get_profile_search_paths(additional_path_list: &Option<Vec<PathBuf>>) -> Vec<PathBuf> {
let mut result = additional_path_list.clone().unwrap_or_default();
if let Some(paths) = env::var_os("ADBC_PROFILE_PATH") {
result.extend(env::split_paths(&paths));
}
#[cfg(conda_build)]
if let Some(conda_prefix) = env::var_os("CONDA_PREFIX") {
result.push(
PathBuf::from(conda_prefix)
.join("etc")
.join("adbc")
.join("profiles"),
);
}
#[cfg(any(target_os = "windows", target_os = "macos"))]
const PROFILE_DIR_NAME: &str = "Profiles";
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
const PROFILE_DIR_NAME: &str = "profiles";
if let Some(profiles_dir) = user_config_dir().and_then(|d| d.parent().map(|p| p.to_path_buf()))
{
result.push(profiles_dir.join(PROFILE_DIR_NAME));
}
result
}
#[derive(Debug)]
pub(crate) enum DriverLocator<'a> {
Uri(&'a str, &'a str),
Profile(&'a str),
}
pub(crate) fn parse_driver_uri(uri: &'_ str) -> Result<DriverLocator<'_>> {
let idx = uri.find(":").ok_or(Error::with_message_and_status(
format!("Invalid URI: {uri}"),
Status::InvalidArguments,
))?;
let driver = &uri[..idx];
if uri.len() <= idx + 2 {
return Ok(DriverLocator::Uri(driver, uri));
}
#[cfg(target_os = "windows")]
if let Ok(true) = std::fs::exists(uri) {
return Ok(DriverLocator::Uri(uri, ""));
}
if &uri[idx..idx + 2] == ":/" {
if driver == "profile" && uri.len() > idx + 2 {
if uri.len() > idx + 3 && &uri[idx + 2..idx + 3] == "/" {
return Ok(DriverLocator::Profile(&uri[idx + 3..]));
}
return Ok(DriverLocator::Profile(&uri[idx + 2..]));
}
return Ok(DriverLocator::Uri(driver, uri));
}
Ok(DriverLocator::Uri(driver, &uri[idx + 1..]))
}
#[cfg(test)]
mod tests {
use std::env;
use adbc_core::{LOAD_FLAG_ALLOW_RELATIVE_PATHS, options::AdbcVersion};
use temp_env::{with_var, with_var_unset};
use crate::ManagedDriver;
use super::*;
fn manifest_without_driver() -> &'static str {
r#"
manifest_version = 1
name = 'SQLite3'
publisher = 'arrow-adbc'
version = '1.0.0'
[ADBC]
version = '1.1.0'
"#
}
fn simple_manifest() -> String {
let driver_path =
PathBuf::from(env::var_os("ADBC_DRIVER_MANAGER_TEST_LIB").expect(
"ADBC_DRIVER_MANAGER_TEST_LIB must be set for driver manager manifest tests",
));
assert!(
driver_path.exists(),
"ADBC_DRIVER_MANAGER_TEST_LIB path does not exist: {}",
driver_path.display()
);
let (os, arch, extra) = arch_triplet();
format!(
r#"
{}
[Driver]
[Driver.shared]
{os}_{arch}{extra} = {driver_path:?}
"#,
manifest_without_driver()
)
}
fn write_manifest_to_tempfile(p: PathBuf, tbl: String) -> (tempfile::TempDir, PathBuf) {
let tmp_dir = tempfile::Builder::new()
.prefix("adbc_tests")
.tempdir()
.expect("Failed to create temporary directory for driver manager manifest test");
let manifest_path = tmp_dir.path().join(p);
if let Some(parent) = manifest_path.parent() {
std::fs::create_dir_all(parent)
.expect("Failed to create parent directory for manifest");
}
std::fs::write(&manifest_path, tbl.as_str())
.expect("Failed to write driver manager manifest to temporary file");
(tmp_dir, manifest_path)
}
#[cfg_attr(target_os = "windows", ignore)] #[test]
fn test_default_entrypoint() {
for driver in [
"adbc_driver_sqlite",
"adbc_driver_sqlite.dll",
"driver_sqlite",
"libadbc_driver_sqlite",
"libadbc_driver_sqlite.so",
"libadbc_driver_sqlite.so.6.0.0",
"/usr/lib/libadbc_driver_sqlite.so",
"/usr/lib/libadbc_driver_sqlite.so.6.0.0",
"C:\\System32\\adbc_driver_sqlite.dll",
] {
assert_eq!(
DriverLibrary::get_default_entrypoint(driver),
"AdbcDriverSqliteInit"
);
}
for driver in [
"adbc_sqlite",
"sqlite",
"/usr/lib/sqlite.so",
"C:\\System32\\sqlite.dll",
] {
assert_eq!(
DriverLibrary::get_default_entrypoint(driver),
"AdbcSqliteInit"
);
}
for driver in [
"proprietary_engine",
"libproprietary_engine.so.6.0.0",
"/usr/lib/proprietary_engine.so",
"C:\\System32\\proprietary_engine.dll",
] {
assert_eq!(
DriverLibrary::get_default_entrypoint(driver),
"AdbcProprietaryEngineInit"
);
}
for driver in ["driver_example", "libdriver_example.so"] {
assert_eq!(
DriverLibrary::get_default_entrypoint(driver),
"AdbcDriverExampleInit"
);
}
}
#[cfg_attr(target_os = "windows", ignore)] #[test]
fn test_default_entrypoint_from_name() {
for name in ["adbc_driver_sqlite", "driver_sqlite"] {
assert_eq!(
DriverLibrary::get_default_entrypoint_from_name(name),
"AdbcDriverSqliteInit"
);
let filename = libloading::library_filename(name);
assert_eq!(
DriverLibrary::get_default_entrypoint(filename),
"AdbcDriverSqliteInit"
);
}
for name in ["adbc_sqlite", "sqlite"] {
assert_eq!(
DriverLibrary::get_default_entrypoint_from_name(name),
"AdbcSqliteInit"
);
let filename = libloading::library_filename(name);
assert_eq!(
DriverLibrary::get_default_entrypoint(filename),
"AdbcSqliteInit"
);
}
}
#[test]
fn test_user_config_dir() {
let _ = user_config_dir().unwrap();
}
#[test]
#[cfg_attr(not(windows), ignore)]
fn test_get_search_paths() {
#[cfg(target_os = "macos")]
let system_path = PathBuf::from("/Library/Application Support/ADBC/Drivers");
#[cfg(not(target_os = "macos"))]
let system_path = PathBuf::from("/etc/adbc/drivers");
let search_paths = get_search_paths(LOAD_FLAG_SEARCH_SYSTEM);
if system_path.exists() {
assert_eq!(search_paths, vec![system_path]);
} else {
assert_eq!(search_paths, Vec::<PathBuf>::new());
}
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_relative_path() {
std::fs::write(PathBuf::from("sqlite.toml"), simple_manifest())
.expect("Failed to write driver manager manifest to file");
let err = ManagedDriver::load_from_name("sqlite.toml", None, AdbcVersion::V100, 0, None)
.unwrap_err();
assert_eq!(err.status, Status::InvalidArguments);
ManagedDriver::load_from_name(
"sqlite.toml",
None,
AdbcVersion::V100,
LOAD_FLAG_ALLOW_RELATIVE_PATHS,
None,
)
.unwrap();
std::fs::remove_file("sqlite.toml")
.expect("Failed to remove temporary driver manifest file");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_manifest_missing_driver() {
let (tmp_dir, manifest_path) = write_manifest_to_tempfile(
PathBuf::from("sqlite.toml"),
manifest_without_driver().to_string(),
);
let err = ManagedDriver::load_from_name(
manifest_path,
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::InvalidArguments);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_manifest_bad_version() {
let manifest = "manifest_version = 2\n".to_owned() + &simple_manifest().to_owned();
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), manifest);
let err = ManagedDriver::load_from_name(
manifest_path,
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::InvalidArguments);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_manifest_wrong_arch() {
let manifest_wrong_arch = format!(
r#"
{}
[Driver]
[Driver.shared]
non-existing = 'path/to/bad/driver.so'
"#,
manifest_without_driver()
);
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), manifest_wrong_arch);
let err = ManagedDriver::load_from_name(
manifest_path,
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::InvalidArguments);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_absolute_path() {
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), simple_manifest());
ManagedDriver::load_from_name(
manifest_path,
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap();
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_absolute_path_no_ext() {
let (tmp_dir, mut manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), simple_manifest());
manifest_path.set_extension("");
ManagedDriver::load_from_name(
manifest_path,
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap();
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
#[cfg_attr(target_os = "windows", ignore)] fn test_load_driver_env() {
with_var_unset("ADBC_DRIVER_PATH", || {
let err = ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_SEARCH_ENV,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::NotFound);
});
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), simple_manifest());
with_var(
"ADBC_DRIVER_PATH",
Some(manifest_path.parent().unwrap().as_os_str()),
|| {
ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_SEARCH_ENV,
None,
)
.unwrap();
},
);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_driver_env_multiple_paths() {
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), simple_manifest());
let path_os_string = env::join_paths([
Path::new("/home"),
Path::new(""),
manifest_path.parent().unwrap(),
])
.unwrap();
with_var("ADBC_DRIVER_PATH", Some(&path_os_string), || {
ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_SEARCH_ENV,
None,
)
.unwrap();
});
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_additional_path() {
let p = PathBuf::from("majestik møøse/sqlite.toml");
let (tmp_dir, manifest_path) = write_manifest_to_tempfile(p, simple_manifest());
ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_SEARCH_ENV,
Some(vec![manifest_path.parent().unwrap().to_path_buf()]),
)
.unwrap();
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
fn test_load_additional_path_with_platform_library_filename() {
let library_path =
DriverLibrary::platform_library_path(Path::new("drivers/adbc_driver_test")).unwrap();
assert_eq!(
library_path.extension(),
Some(OsStr::new(env::consts::DLL_EXTENSION))
);
assert_eq!(
library_path,
Path::new("drivers").join(libloading::library_filename("adbc_driver_test"))
);
}
#[test]
fn test_platform_library_path_requires_file_name() {
let err = DriverLibrary::platform_library_path(Path::new("")).unwrap_err();
assert_eq!(err.status, Status::Internal);
assert_eq!(err.message, "Driver search path must include a file name");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
fn test_load_non_ascii_path() {
let p = PathBuf::from("majestik møøse/sqlite.toml");
let (tmp_dir, manifest_path) = write_manifest_to_tempfile(p, simple_manifest());
with_var(
"ADBC_DRIVER_PATH",
Some(manifest_path.parent().unwrap().as_os_str()),
|| {
ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_SEARCH_ENV,
None,
)
.unwrap();
},
);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(not(feature = "driver_manager_test_lib"), ignore)]
#[cfg_attr(target_os = "windows", ignore)] fn test_disallow_env_config() {
let (tmp_dir, manifest_path) =
write_manifest_to_tempfile(PathBuf::from("sqlite.toml"), simple_manifest());
with_var(
"ADBC_DRIVER_PATH",
Some(manifest_path.parent().unwrap().as_os_str()),
|| {
let load_flags = adbc_core::LOAD_FLAG_DEFAULT & !adbc_core::LOAD_FLAG_SEARCH_ENV;
let err = ManagedDriver::load_from_name(
"sqlite",
None,
AdbcVersion::V100,
load_flags,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::NotFound);
},
);
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
#[cfg_attr(
not(all(
feature = "driver_manager_test_lib",
feature = "driver_manager_test_manifest_user"
)),
ignore
)]
#[cfg_attr(target_os = "windows", ignore)] fn test_manifest_user_config() {
let err = ManagedDriver::load_from_name(
"adbc-test-sqlite",
None,
AdbcVersion::V110,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::NotFound);
let usercfg_dir = user_config_dir().unwrap();
let mut created = false;
if !usercfg_dir.exists() {
std::fs::create_dir_all(&usercfg_dir)
.expect("Failed to create user config directory for driver manager test");
created = true;
}
let manifest_path = usercfg_dir.join("adbc-test-sqlite.toml");
std::fs::write(&manifest_path, simple_manifest())
.expect("Failed to write driver manager manifest to user config directory");
let err = ManagedDriver::load_from_name(
"adbc-test-sqlite",
None,
AdbcVersion::V110,
adbc_core::LOAD_FLAG_DEFAULT & !adbc_core::LOAD_FLAG_SEARCH_USER,
None,
)
.unwrap_err();
assert_eq!(err.status, Status::NotFound);
ManagedDriver::load_from_name(
"adbc-test-sqlite",
None,
AdbcVersion::V110,
adbc_core::LOAD_FLAG_SEARCH_USER,
None,
)
.unwrap();
std::fs::remove_file(&manifest_path)
.expect("Failed to remove driver manager manifest from user config directory");
if created {
std::fs::remove_dir(usercfg_dir).unwrap();
}
}
#[test]
#[cfg(target_os = "windows")]
fn test_load_driver_from_registry() {
use std::ffi::OsStr;
let driver_name = OsStr::new("nonexistent_test_driver");
let result =
DriverLibrary::load_library_from_registry(windows_registry::CURRENT_USER, driver_name);
match result {
Ok(_) => panic!("Expected registry lookup to fail"),
Err(err) => assert_eq!(err.status, Status::NotFound),
}
}
#[test]
fn test_parse_driver_uri() {
let cases = vec![
("sqlite", Err(Status::InvalidArguments)),
("sqlite:", Ok(DriverLocator::Uri("sqlite", "sqlite:"))),
(
"sqlite:file::memory:",
Ok(DriverLocator::Uri("sqlite", "file::memory:")),
),
(
"sqlite:file::memory:?cache=shared",
Ok(DriverLocator::Uri("sqlite", "file::memory:?cache=shared")),
),
(
"postgresql://a:b@localhost:9999/nonexistent",
Ok(DriverLocator::Uri(
"postgresql",
"postgresql://a:b@localhost:9999/nonexistent",
)),
),
(
"profile://my_profile",
Ok(DriverLocator::Profile("my_profile")),
),
(
"profile://path/to/profile.toml",
Ok(DriverLocator::Profile("path/to/profile.toml")),
),
(
"profile:///absolute/path/to/profile.toml",
Ok(DriverLocator::Profile("/absolute/path/to/profile.toml")),
),
("invalid_uri", Err(Status::InvalidArguments)),
];
for (input, expected) in cases {
let result = parse_driver_uri(input);
match expected {
Ok(DriverLocator::Uri(exp_driver, exp_conn)) => {
let DriverLocator::Uri(driver, conn) = result.expect("Expected Ok result")
else {
panic!("Expected DriverUri result");
};
assert_eq!(driver, exp_driver);
assert_eq!(conn, exp_conn);
}
Ok(DriverLocator::Profile(exp_profile)) => {
let DriverLocator::Profile(profile) = result.expect("Expected Ok result")
else {
panic!("Expected Profile result");
};
assert_eq!(profile, exp_profile);
}
Err(exp_status) => {
let err = result.expect_err("Expected Err result");
assert_eq!(err.status, exp_status);
}
}
}
}
#[test]
fn test_load_from_name_includes_search_trace_in_error_details() {
let err = ManagedDriver::load_from_name(
"adbc_test_driver_manager_missing_driver_7c8f5482",
None,
AdbcVersion::V100,
adbc_core::LOAD_FLAG_DEFAULT,
None,
)
.unwrap_err();
let details = err
.details
.expect("Expected load-from-name errors to include details");
let trace = details
.iter()
.find(|(key, _)| key == "adbc.drivermanager.driver_load_trace")
.expect("Expected driver load trace in error details");
assert!(
!trace.1.is_empty(),
"Expected driver load trace detail to be non-empty"
);
}
#[cfg(target_os = "windows")]
#[test]
fn test_parse_driver_uri_windows_file() {
let tmp_dir = tempfile::Builder::new()
.prefix("adbc_tests")
.tempdir()
.expect("Failed to create temporary directory for driver manager manifest test");
let temp_db_path = tmp_dir.path().join("test.dll");
if let Some(parent) = temp_db_path.parent() {
std::fs::create_dir_all(parent)
.expect("Failed to create parent directory for manifest");
}
std::fs::write(&temp_db_path, b"").expect("Failed to create temporary database file");
let DriverLocator::Uri(driver, conn) =
parse_driver_uri(temp_db_path.to_str().unwrap()).unwrap()
else {
panic!("Expected DriverUri result");
};
assert_eq!(driver, temp_db_path.to_str().unwrap());
assert_eq!(conn, "");
tmp_dir
.close()
.expect("Failed to close/remove temporary directory");
}
#[test]
fn test_find_filesystem_profile() {
let test_cases = vec![
(
"absolute path with extension",
"test_profile.toml",
None,
true,
true,
),
(
"relative name without extension",
"my_profile",
Some(vec![]),
false,
true,
),
(
"relative name with extension",
"my_profile.toml",
Some(vec![]),
false,
true,
),
(
"absolute path without extension",
"profile_no_ext",
None,
true,
true,
),
(
"nonexistent profile",
"nonexistent_profile",
None,
false,
false,
),
];
for (name, profile_name, search_paths_opt, is_absolute, should_succeed) in test_cases {
let tmp_dir = tempfile::Builder::new()
.prefix("adbc_profile_test")
.tempdir()
.unwrap();
let expected_profile_path = tmp_dir.path().join(if profile_name.ends_with(".toml") {
profile_name.to_string()
} else {
format!("{}.toml", profile_name)
});
if should_succeed {
std::fs::write(&expected_profile_path, "test content").unwrap();
}
let search_paths = search_paths_opt.map(|mut paths| {
paths.push(tmp_dir.path().to_path_buf());
paths
});
let profile_arg = if is_absolute {
if profile_name.ends_with(".toml") {
expected_profile_path.to_str().unwrap().to_string()
} else {
tmp_dir
.path()
.join(profile_name)
.to_str()
.unwrap()
.to_string()
}
} else {
profile_name.to_string()
};
let result = find_filesystem_profile(&profile_arg, &search_paths);
if should_succeed {
assert!(
result.is_ok(),
"Test case '{}' failed: {:?}",
name,
result.err()
);
assert_eq!(
result.unwrap(),
expected_profile_path,
"Test case '{}': path mismatch",
name
);
} else {
assert!(result.is_err(), "Test case '{}': expected error", name);
let err = result.unwrap_err();
assert_eq!(
err.status,
Status::NotFound,
"Test case '{}': wrong error status",
name
);
assert!(
err.message.contains("Profile not found"),
"Test case '{}': wrong error message",
name
);
}
tmp_dir.close().unwrap();
}
}
#[test]
fn test_find_filesystem_profile_search_multiple_paths() {
let tmp_dir1 = tempfile::Builder::new()
.prefix("adbc_profile_test1")
.tempdir()
.unwrap();
let tmp_dir2 = tempfile::Builder::new()
.prefix("adbc_profile_test2")
.tempdir()
.unwrap();
let profile_path = tmp_dir2.path().join("searched_profile.toml");
std::fs::write(&profile_path, "test content").unwrap();
let result = find_filesystem_profile(
"searched_profile",
&Some(vec![
tmp_dir1.path().to_path_buf(),
tmp_dir2.path().to_path_buf(),
]),
);
assert!(result.is_ok());
assert_eq!(result.unwrap(), profile_path);
tmp_dir1.close().unwrap();
tmp_dir2.close().unwrap();
}
#[test]
fn test_get_profile_search_paths() {
let tmp_dir = tempfile::Builder::new()
.prefix("adbc_profile_test")
.tempdir()
.unwrap();
let paths = get_profile_search_paths(&Some(vec![tmp_dir.path().to_path_buf()]));
assert!(paths.contains(&tmp_dir.path().to_path_buf()));
assert!(!paths.is_empty());
tmp_dir.close().unwrap();
}
#[test]
fn test_get_profile_search_paths_empty() {
let paths = get_profile_search_paths(&None);
assert!(!paths.is_empty() || paths.is_empty()); }
}