use std::fs;
use std::path::{Path, PathBuf};
use khive_storage::tx_registry::DbIdentity;
use rusqlite::Connection;
use crate::error::SqliteError;
#[cfg(windows)]
use crate::file_identity::sqlite_opened_file_identity;
#[cfg(any(unix, windows))]
use crate::file_identity::{database_file_identity, DatabaseFileIdentity};
use super::{WriteAdmission, DATABASE_ID_TABLE};
const MAX_SYMLINK_DEPTH: u32 = 40;
pub(super) fn mint_db_identity(
configured_path: &Path,
) -> Result<(DbIdentity, PathBuf), SqliteError> {
let absolute = if configured_path.is_absolute() {
configured_path.to_path_buf()
} else {
let cwd = std::env::current_dir().map_err(|e| {
SqliteError::InvalidData(format!(
"cannot mint database identity for {configured_path:?}: failed to resolve the \
process current directory: {e}"
))
})?;
cwd.join(configured_path)
};
if absolute.exists() {
let canonical = absolute.canonicalize().map_err(|e| {
SqliteError::InvalidData(format!(
"cannot mint database identity: failed to canonicalize existing path \
{absolute:?}: {e}"
))
})?;
return Ok((
DbIdentity::new(canonical.clone().into_os_string()),
canonical,
));
}
let resolved_target = resolve_symlink_chain(&absolute)?;
let parent = resolved_target.parent().ok_or_else(|| {
SqliteError::InvalidData(format!(
"cannot mint database identity for {resolved_target:?}: path has no parent \
directory"
))
})?;
let file_name = resolved_target.file_name().ok_or_else(|| {
SqliteError::InvalidData(format!(
"cannot mint database identity for {resolved_target:?}: path has no file name"
))
})?;
let canonical_parent = parent.canonicalize().map_err(|e| {
SqliteError::InvalidData(format!(
"cannot mint database identity: parent directory {parent:?} of first-open path \
{resolved_target:?} does not exist or is inaccessible: {e}"
))
})?;
let mut identity_path = canonical_parent;
identity_path.push(file_name);
Ok((
DbIdentity::new(identity_path.clone().into_os_string()),
identity_path,
))
}
#[cfg(any(unix, windows))]
pub(super) fn database_file_identity_if_exists(
path: &Path,
) -> Result<Option<DatabaseFileIdentity>, SqliteError> {
match database_file_identity(path) {
Ok(identity) => Ok(Some(identity)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error.into()),
}
}
#[cfg(any(unix, windows))]
pub(crate) fn opened_sqlite_file_identity(
conn: &Connection,
path: &Path,
) -> Result<DatabaseFileIdentity, SqliteError> {
#[cfg(unix)]
{
verify_sqlite_opened_file_still_at_path(conn)?;
Ok(database_file_identity(path)?)
}
#[cfg(windows)]
{
let opened = sqlite_opened_file_identity(conn)?;
if database_file_identity(path).ok() != Some(opened) {
return Err(SqliteError::InvalidData(
"database file identity changed while SQLite held the opened file".to_string(),
));
}
Ok(opened)
}
}
pub(super) fn read_database_id(conn: &Connection) -> Result<Option<uuid::Uuid>, SqliteError> {
let table_exists: bool = conn.query_row(
"SELECT count(*) != 0 FROM main.sqlite_master WHERE type = 'table' AND name = ?1",
[DATABASE_ID_TABLE],
|row| row.get(0),
)?;
if !table_exists {
return Ok(None);
}
let id: String = conn.query_row(
&format!("SELECT id FROM main.{DATABASE_ID_TABLE} WHERE singleton = 1"),
[],
|row| row.get(0),
)?;
let id = uuid::Uuid::parse_str(&id).map_err(|error| {
SqliteError::InvalidData(format!("invalid stored database identity: {error}"))
})?;
Ok(Some(id))
}
#[cfg(test)]
pub(super) fn initialize_database_id(conn: &mut Connection) -> Result<uuid::Uuid, SqliteError> {
initialize_database_id_with_admission(conn, None)
}
pub(super) fn initialize_database_id_with_admission(
conn: &mut Connection,
admission: Option<&WriteAdmission>,
) -> Result<uuid::Uuid, SqliteError> {
if let Some(id) = read_database_id(conn)? {
return Ok(id);
}
let transaction = conn.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
if let Some(admission) = admission {
if let Err(error) = admission.check() {
let rollback = transaction.rollback();
return Err(crate::migrations::capacity_refusal_after_rollback(
conn,
rollback,
error,
"database identity bootstrap",
));
}
}
transaction.execute_batch(&format!(
"CREATE TABLE IF NOT EXISTS main.{DATABASE_ID_TABLE} (\
singleton INTEGER PRIMARY KEY CHECK (singleton = 1), \
id TEXT NOT NULL\
)"
))?;
transaction.execute(
&format!("INSERT OR IGNORE INTO main.{DATABASE_ID_TABLE} (singleton, id) VALUES (1, ?1)"),
[uuid::Uuid::new_v4().to_string()],
)?;
let id: String = transaction.query_row(
&format!("SELECT id FROM main.{DATABASE_ID_TABLE} WHERE singleton = 1"),
[],
|row| row.get(0),
)?;
let id = uuid::Uuid::parse_str(&id).map_err(|error| {
SqliteError::InvalidData(format!("invalid stored database identity: {error}"))
})?;
transaction.commit()?;
Ok(id)
}
#[cfg(unix)]
fn verify_sqlite_opened_file_still_at_path(conn: &Connection) -> Result<(), SqliteError> {
let mut moved: std::ffi::c_int = 0;
let result = unsafe {
rusqlite::ffi::sqlite3_file_control(
conn.handle(),
c"main".as_ptr(),
rusqlite::ffi::SQLITE_FCNTL_HAS_MOVED,
(&mut moved as *mut std::ffi::c_int).cast(),
)
};
if result != rusqlite::ffi::SQLITE_OK {
return Err(SqliteError::InvalidData(format!(
"cannot verify opened database file identity (SQLite file control {result})"
)));
}
if moved != 0 {
return Err(SqliteError::InvalidData(
"database file identity changed while SQLite held the opened file".to_string(),
));
}
Ok(())
}
fn resolve_symlink_chain(path: &Path) -> Result<PathBuf, SqliteError> {
let mut current = path.to_path_buf();
for _ in 0..MAX_SYMLINK_DEPTH {
match fs::symlink_metadata(¤t) {
Ok(meta) if meta.file_type().is_symlink() => {
let target = fs::read_link(¤t).map_err(|e| {
SqliteError::InvalidData(format!(
"cannot mint database identity: failed to read symlink {current:?}: {e}"
))
})?;
current = if target.is_absolute() {
target
} else {
match current.parent() {
Some(parent) => parent.join(&target),
None => target,
}
};
}
_ => return Ok(current),
}
}
Err(SqliteError::InvalidData(format!(
"cannot mint database identity for {path:?}: symlink chain exceeds \
{MAX_SYMLINK_DEPTH} levels"
)))
}