pub mod result;
pub mod query_engine;
pub mod ffi;
pub mod shared;
mod dynamic;
use result::{SdkResult, SdkError};
use dynamic::NativeDB;
use serde_json::Value;
use std::path::Path;
use std::time::Instant;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct SecretKey(Vec<u8>);
impl SecretKey {
pub fn new(bytes: Vec<u8>) -> Self {
Self(bytes)
}
pub fn from_env(env_var: &str) -> SdkResult<Self> {
let val = std::env::var(env_var).map_err(|_| {
SdkError::SecurityError(format!(
"Encryption key env var '{}' is not set. \
Set it with: $env:{}=\"your-secret-key\" (PowerShell) \
or export {}=\"your-secret-key\" (bash)",
env_var, env_var, env_var
))
})?;
if val.is_empty() {
return Err(SdkError::SecurityError(format!(
"Encryption key env var '{}' is set but empty.", env_var
)));
}
Ok(Self(val.into_bytes()))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
pub fn set_secure_permissions(path: &str) -> SdkResult<()> {
#[cfg(target_os = "windows")]
{
let output = std::process::Command::new("icacls")
.args([path, "/inheritance:r", "/grant:r", "%USERNAME%:F"])
.output();
match output {
Ok(out) if out.status.success() => {}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
eprintln!("[overdrive-sdk] WARNING: Could not set file permissions on '{}': {}", path, stderr);
}
Err(e) => {
eprintln!("[overdrive-sdk] WARNING: icacls not available, file permissions not hardened: {}", e);
}
}
}
#[cfg(not(target_os = "windows"))]
{
use std::os::unix::fs::PermissionsExt;
if Path::new(path).exists() {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.map_err(|e| SdkError::SecurityError(format!(
"Failed to chmod 600 '{}': {}", path, e
)))?;
}
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct QueryResult {
pub rows: Vec<Value>,
pub columns: Vec<String>,
pub rows_affected: usize,
pub execution_time_ms: f64,
}
impl QueryResult {
fn empty() -> Self {
Self {
rows: Vec::new(),
columns: Vec::new(),
rows_affected: 0,
execution_time_ms: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct Stats {
pub tables: usize,
pub total_records: usize,
pub file_size_bytes: u64,
pub path: String,
pub mvcc_active_versions: usize,
pub page_size: usize,
pub sdk_version: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IsolationLevel {
ReadUncommitted = 0,
ReadCommitted = 1,
RepeatableRead = 2,
Serializable = 3,
}
impl IsolationLevel {
pub fn from_i32(val: i32) -> Self {
match val {
0 => IsolationLevel::ReadUncommitted,
1 => IsolationLevel::ReadCommitted,
2 => IsolationLevel::RepeatableRead,
3 => IsolationLevel::Serializable,
_ => IsolationLevel::ReadCommitted,
}
}
}
#[derive(Debug, Clone)]
pub struct TransactionHandle {
pub txn_id: u64,
pub isolation: IsolationLevel,
pub active: bool,
}
#[derive(Debug, Clone)]
pub struct IntegrityReport {
pub is_valid: bool,
pub pages_checked: usize,
pub tables_verified: usize,
pub issues: Vec<String>,
}
pub struct OverDriveDB {
native: NativeDB,
path: String,
}
impl OverDriveDB {
pub fn open(path: &str) -> SdkResult<Self> {
let native = NativeDB::open(path)?;
let _ = set_secure_permissions(path);
Ok(Self {
native,
path: path.to_string(),
})
}
pub fn create(path: &str) -> SdkResult<Self> {
if Path::new(path).exists() {
return Err(SdkError::DatabaseAlreadyExists(path.to_string()));
}
Self::open(path)
}
pub fn open_existing(path: &str) -> SdkResult<Self> {
if !Path::new(path).exists() {
return Err(SdkError::DatabaseNotFound(path.to_string()));
}
Self::open(path)
}
pub fn sync(&self) -> SdkResult<()> {
self.native.sync();
Ok(())
}
pub fn close(mut self) -> SdkResult<()> {
self.native.close();
Ok(())
}
pub fn destroy(path: &str) -> SdkResult<()> {
if Path::new(path).exists() {
std::fs::remove_file(path)?;
}
Ok(())
}
pub fn path(&self) -> &str {
&self.path
}
pub fn version() -> String {
NativeDB::version()
}
pub fn open_encrypted(path: &str, key_env_var: &str) -> SdkResult<Self> {
let key = SecretKey::from_env(key_env_var)?;
std::env::set_var("__OVERDRIVE_KEY", std::str::from_utf8(key.as_bytes())
.map_err(|_| SdkError::SecurityError("Key contains non-UTF8 bytes".to_string()))?);
let db = Self::open(path)?;
std::env::remove_var("__OVERDRIVE_KEY");
Ok(db)
}
pub fn backup(&self, dest_path: &str) -> SdkResult<()> {
self.sync()?;
std::fs::copy(&self.path, dest_path)
.map_err(|e| SdkError::BackupError(format!(
"Failed to copy '{}' -> '{}': {}", self.path, dest_path, e
)))?;
let wal_src = format!("{}.wal", self.path);
let wal_dst = format!("{}.wal", dest_path);
if Path::new(&wal_src).exists() {
std::fs::copy(&wal_src, &wal_dst)
.map_err(|e| SdkError::BackupError(format!(
"Failed to copy WAL '{}' -> '{}': {}", wal_src, wal_dst, e
)))?;
}
let _ = set_secure_permissions(dest_path);
Ok(())
}
pub fn cleanup_wal(&self) -> SdkResult<()> {
let wal_path = format!("{}.wal", self.path);
if Path::new(&wal_path).exists() {
std::fs::remove_file(&wal_path)
.map_err(|e| SdkError::IoError(e))?;
}
Ok(())
}
pub fn create_table(&mut self, name: &str) -> SdkResult<()> {
self.native.create_table(name)?;
Ok(())
}
pub fn drop_table(&mut self, name: &str) -> SdkResult<()> {
self.native.drop_table(name)?;
Ok(())
}
pub fn list_tables(&self) -> SdkResult<Vec<String>> {
Ok(self.native.list_tables()?)
}
pub fn table_exists(&self, name: &str) -> SdkResult<bool> {
Ok(self.native.table_exists(name))
}
pub fn insert(&mut self, table: &str, doc: &Value) -> SdkResult<String> {
let json_str = serde_json::to_string(doc)?;
let id = self.native.insert(table, &json_str)?;
Ok(id)
}
pub fn insert_batch(&mut self, table: &str, docs: &[Value]) -> SdkResult<Vec<String>> {
let mut ids = Vec::with_capacity(docs.len());
for doc in docs {
let id = self.insert(table, doc)?;
ids.push(id);
}
Ok(ids)
}
pub fn get(&self, table: &str, id: &str) -> SdkResult<Option<Value>> {
match self.native.get(table, id)? {
Some(json_str) => {
let value: Value = serde_json::from_str(&json_str)?;
Ok(Some(value))
}
None => Ok(None),
}
}
pub fn update(&mut self, table: &str, id: &str, updates: &Value) -> SdkResult<bool> {
let json_str = serde_json::to_string(updates)?;
Ok(self.native.update(table, id, &json_str)?)
}
pub fn delete(&mut self, table: &str, id: &str) -> SdkResult<bool> {
Ok(self.native.delete(table, id)?)
}
pub fn count(&self, table: &str) -> SdkResult<usize> {
let count = self.native.count(table)?;
Ok(count.max(0) as usize)
}
pub fn scan(&self, table: &str) -> SdkResult<Vec<Value>> {
let result_str = self.native.query(&format!("SELECT * FROM {}", table))?;
let result: Value = serde_json::from_str(&result_str)?;
let rows = result.get("rows")
.and_then(|r| r.as_array())
.cloned()
.unwrap_or_default();
Ok(rows)
}
pub fn query(&mut self, sql: &str) -> SdkResult<QueryResult> {
let start = Instant::now();
let result_str = self.native.query(sql)?;
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
let result: Value = serde_json::from_str(&result_str)?;
let rows = result.get("rows")
.and_then(|r| r.as_array())
.cloned()
.unwrap_or_default();
let columns = result.get("columns")
.and_then(|c| c.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
let rows_affected = result.get("rows_affected")
.and_then(|r| r.as_u64())
.unwrap_or(0) as usize;
Ok(QueryResult {
rows,
columns,
rows_affected,
execution_time_ms: elapsed,
})
}
pub fn query_safe(&mut self, sql_template: &str, params: &[&str]) -> SdkResult<QueryResult> {
const DANGEROUS: &[&str] = &[
"DROP", "TRUNCATE", "ALTER", "EXEC", "EXECUTE",
"--", ";--", "/*", "*/", "xp_", "UNION",
];
let mut sanitized: Vec<String> = Vec::with_capacity(params.len());
for ¶m in params {
let upper = param.to_uppercase();
for &danger in DANGEROUS {
if upper.contains(danger) {
return Err(SdkError::SecurityError(format!(
"SQL injection detected in parameter: '{}' contains forbidden token '{}'",
param, danger
)));
}
}
let escaped = param.replace('\'', "''");
sanitized.push(format!("'{}'", escaped));
}
let mut sql = sql_template.to_string();
for value in &sanitized {
if let Some(pos) = sql.find('?') {
sql.replace_range(pos..pos + 1, value);
} else {
return Err(SdkError::SecurityError(
"More params than '?' placeholders in SQL template".to_string()
));
}
}
let remaining = params.len();
let placeholder_count = sql_template.chars().filter(|&c| c == '?').count();
if remaining < placeholder_count {
return Err(SdkError::SecurityError(format!(
"SQL template has {} '?' placeholders but only {} params were provided",
placeholder_count, remaining
)));
}
self.query(&sql)
}
pub fn search(&self, table: &str, text: &str) -> SdkResult<Vec<Value>> {
let result_str = self.native.search(table, text)?;
let values: Vec<Value> = serde_json::from_str(&result_str).unwrap_or_default();
Ok(values)
}
pub fn stats(&self) -> SdkResult<Stats> {
let file_size = std::fs::metadata(&self.path)
.map(|m| m.len())
.unwrap_or(0);
let tables = self.list_tables().unwrap_or_default();
let mut total_records = 0;
for table in &tables {
total_records += self.count(table).unwrap_or(0);
}
Ok(Stats {
tables: tables.len(),
total_records,
file_size_bytes: file_size,
path: self.path.clone(),
mvcc_active_versions: 0, page_size: 4096,
sdk_version: Self::version(),
})
}
pub fn begin_transaction(&mut self, isolation: IsolationLevel) -> SdkResult<TransactionHandle> {
let txn_id = self.native.begin_transaction(isolation as i32)?;
Ok(TransactionHandle {
txn_id,
isolation,
active: true,
})
}
pub fn commit_transaction(&mut self, txn: &TransactionHandle) -> SdkResult<()> {
self.native.commit_transaction(txn.txn_id)?;
Ok(())
}
pub fn abort_transaction(&mut self, txn: &TransactionHandle) -> SdkResult<()> {
self.native.abort_transaction(txn.txn_id)?;
Ok(())
}
pub fn verify_integrity(&self) -> SdkResult<IntegrityReport> {
let result_str = self.native.verify_integrity()?;
let result: Value = serde_json::from_str(&result_str).unwrap_or_default();
let is_valid = result.get("valid")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let pages_checked = result.get("pages_checked")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let tables_verified = result.get("tables_verified")
.and_then(|v| v.as_u64())
.unwrap_or(0) as usize;
let issues = result.get("issues")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
Ok(IntegrityReport {
is_valid,
pages_checked,
tables_verified,
issues,
})
}
}
impl Drop for OverDriveDB {
fn drop(&mut self) {
self.native.close();
}
}