use std::boxed::Box;
use std::ffi::OsStr;
use std::fmt;
use std::format;
use std::string::{String, ToString};
use std::sync::{Mutex, OnceLock};
use std::vec::Vec;
use libloading::Library;
const MINIMUM_FFI_VERSION: u32 = 20191214;
const MAX_BOTAN3_ABI_REV: u32 = 22;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LoadError {
AlreadyLoaded,
LoadFailed(String),
UnusableLibrary(String),
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AlreadyLoaded => write!(f, "A Botan library has already been loaded"),
Self::LoadFailed(msg) => write!(f, "Unable to load the Botan library: {msg}"),
Self::UnusableLibrary(msg) => write!(f, "The loaded library is not usable: {msg}"),
}
}
}
impl std::error::Error for LoadError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Unavailable {
Function,
Library,
}
pub(crate) struct Symbol<F: Copy + 'static> {
name: &'static str,
cell: OnceLock<Option<F>>,
}
impl<F: Copy + 'static> Symbol<F> {
pub(crate) const fn new(name: &'static str) -> Self {
Self {
name,
cell: OnceLock::new(),
}
}
pub(crate) unsafe fn get(&'static self) -> Result<F, Unavailable> {
let lib = library().map_err(|_| Unavailable::Library)?;
let sym = self.cell.get_or_init(|| {
unsafe { lib.get::<F>(self.name.as_bytes()) }
.ok()
.map(|s| *s)
});
sym.ok_or(Unavailable::Function)
}
}
struct Loaded {
lib: Library,
name: String,
}
enum State {
Unattempted,
AutoFailed(&'static str),
}
static LIBRARY: OnceLock<Loaded> = OnceLock::new();
static STATE: Mutex<State> = Mutex::new(State::Unattempted);
fn lock_state() -> std::sync::MutexGuard<'static, State> {
STATE.lock().unwrap_or_else(|e| e.into_inner())
}
fn library() -> Result<&'static Library, &'static str> {
if let Some(loaded) = LIBRARY.get() {
return Ok(&loaded.lib);
}
let mut state = lock_state();
if let Some(loaded) = LIBRARY.get() {
return Ok(&loaded.lib);
}
match *state {
State::AutoFailed(msg) => Err(msg),
State::Unattempted => match default_search() {
Ok(loaded) => {
let _ = LIBRARY.set(loaded);
Ok(&LIBRARY.get().expect("just set").lib)
}
Err(e) => {
let msg: &'static str = Box::leak(e.to_string().into_boxed_str());
*state = State::AutoFailed(msg);
Err(msg)
}
},
}
}
#[cfg(windows)]
unsafe fn open_library(name: &OsStr) -> Result<Library, libloading::Error> {
use libloading::os::windows::{
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR, Library as WinLibrary,
};
let mut flags = LOAD_LIBRARY_SEARCH_DEFAULT_DIRS;
if std::path::Path::new(name).is_absolute() {
flags |= LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR;
}
unsafe { WinLibrary::load_with_flags(name, flags) }.map(Library::from)
}
#[cfg(not(windows))]
unsafe fn open_library(name: &OsStr) -> Result<Library, libloading::Error> {
unsafe { Library::new(name) }
}
fn try_load(name: &OsStr) -> Result<Loaded, LoadError> {
let lib = unsafe { open_library(name) }.map_err(|e| LoadError::LoadFailed(e.to_string()))?;
let api_version =
unsafe { lib.get::<unsafe extern "C" fn() -> u32>(b"botan_ffi_api_version\0") }.map_err(
|_| {
LoadError::UnusableLibrary(format!(
"{} does not export botan_ffi_api_version",
name.to_string_lossy()
))
},
)?;
let version = unsafe { api_version() };
if version < MINIMUM_FFI_VERSION {
return Err(LoadError::UnusableLibrary(format!(
"{} has FFI version {} but at least {} is required",
name.to_string_lossy(),
version,
MINIMUM_FFI_VERSION
)));
}
Ok(Loaded {
lib,
name: name.to_string_lossy().into_owned(),
})
}
fn candidate_names() -> Vec<String> {
let mut names = Vec::new();
if cfg!(target_os = "windows") {
names.push("botan-3.dll".to_string());
names.push("botan.dll".to_string()); } else if cfg!(target_os = "macos") {
let prefixes = ["", "/opt/homebrew/lib/", "/usr/local/lib/"];
for prefix in prefixes {
for rev in (0..=MAX_BOTAN3_ABI_REV).rev() {
names.push(format!("{prefix}libbotan-3.{rev}.dylib"));
}
names.push(format!("{prefix}libbotan-3.dylib"));
}
for prefix in prefixes {
for rev in (13..=19).rev() {
names.push(format!("{prefix}libbotan-2.{rev}.dylib"));
}
names.push(format!("{prefix}libbotan-2.dylib"));
}
} else {
for rev in (0..=MAX_BOTAN3_ABI_REV).rev() {
names.push(format!("libbotan-3.so.{rev}"));
}
names.push("libbotan-3.so".to_string());
for rev in (13..=19).rev() {
names.push(format!("libbotan-2.so.{rev}"));
}
names.push("libbotan-2.so".to_string());
}
names
}
fn default_search() -> Result<Loaded, LoadError> {
let names = candidate_names();
let mut last_error = String::new();
for name in &names {
match try_load(OsStr::new(name)) {
Ok(loaded) => return Ok(loaded),
Err(LoadError::LoadFailed(msg) | LoadError::UnusableLibrary(msg)) => {
last_error = msg;
}
Err(e) => last_error = e.to_string(),
}
}
Err(LoadError::LoadFailed(format!(
"no Botan shared library found; tried {} names such as {} and {} (last error: {}). \
Use botan_sys::load_library to specify the location explicitly.",
names.len(),
names.first().map(String::as_str).unwrap_or(""),
names.last().map(String::as_str).unwrap_or(""),
last_error
)))
}
pub fn load_library<P: AsRef<OsStr>>(path: P) -> Result<(), LoadError> {
let _state = lock_state();
if LIBRARY.get().is_some() {
return Err(LoadError::AlreadyLoaded);
}
let loaded = try_load(path.as_ref())?;
let _ = LIBRARY.set(loaded);
Ok(())
}
pub fn loaded_library_name() -> Option<&'static str> {
LIBRARY.get().map(|l| l.name.as_str())
}
pub fn last_load_error() -> Option<&'static str> {
if LIBRARY.get().is_some() {
return None;
}
match *lock_state() {
State::AutoFailed(msg) => Some(msg),
State::Unattempted => None,
}
}