use std::{
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use kcode_k1_access_profile_sqlite::{OpenError, ProfileDatabase, Snapshot};
use kcode_k1_transaction::Transaction;
use kcode_k1_txn_ordering::K1TxnOrdering;
pub use kcode_k1_access_profile_sqlite::{ApplyOutcome, ProfileAction, ProfileName, SavedProfile};
pub use kcode_k1_access_profile_types::{
AuthorizationProfile, ProfileId, ProfileRevision, TxId, UserId,
};
const SUBSYSTEM: &str = "k1-profile-subsystem";
pub struct ProfileStore {
database: ProfileDatabase,
ordering: Arc<K1TxnOrdering>,
available: AtomicBool,
}
impl ProfileStore {
pub fn open(root: &Path, ordering: Arc<K1TxnOrdering>) -> Result<(Self, Option<TxId>), String> {
let started = Instant::now();
let result = Self::open_inner(root, ordering);
if started.elapsed() > Duration::from_millis(100) {
let outcome = if result.is_ok() { "ready" } else { "error" };
eprintln!(
"level=warn module=kcode-k1-access-profile-store operation=open elapsed_us={} outcome={outcome}",
started.elapsed().as_micros()
);
}
result
}
fn open_inner(
root: &Path,
ordering: Arc<K1TxnOrdering>,
) -> Result<(Self, Option<TxId>), String> {
let (database, snapshot) = match ProfileDatabase::open(root) {
Ok(value) => value,
Err(OpenError::Rebuildable) => ProfileDatabase::rebuild(root)?,
Err(OpenError::Fatal(error)) => return Err(error),
};
let (database, snapshot) = match validate_snapshot(&ordering, &snapshot) {
Ok(()) => (database, snapshot),
Err(ValidationIssue::Invalid) => {
drop(database);
ProfileDatabase::rebuild(root)?
}
Err(ValidationIssue::Fatal(error)) => return Err(error),
};
let cursor = snapshot.cursor();
Ok((
Self {
database,
ordering,
available: AtomicBool::new(true),
},
cursor,
))
}
pub fn apply(
&self,
callback_txid: TxId,
action: ProfileAction,
) -> Result<ApplyOutcome, String> {
self.require_available()?;
if let Err(issue) = validate_transaction(&self.ordering, callback_txid) {
return match issue {
ValidationIssue::Invalid => self.disable("profile callback contradicts KTO"),
ValidationIssue::Fatal(error) => self.disable(error),
};
}
self.require_available()?;
match self.database.apply(callback_txid, action) {
Ok(outcome) => Ok(outcome),
Err(error) => self.disable(error),
}
}
pub fn get_for_user(
&self,
user: UserId,
profile_id: ProfileId,
) -> Result<Option<SavedProfile>, String> {
self.require_available()?;
match self.database.get_for_user(user, profile_id) {
Ok(profile) => Ok(profile),
Err(error) => self.disable(error),
}
}
pub fn list_for_user(&self, user: UserId) -> Result<Vec<SavedProfile>, String> {
self.require_available()?;
match self.database.list_for_user(user) {
Ok(profiles) => Ok(profiles),
Err(error) => self.disable(error),
}
}
pub fn clear(&self) -> Result<(), String> {
self.require_available()?;
match self.database.clear() {
Ok(()) => Ok(()),
Err(error) => self.disable(error),
}
}
fn require_available(&self) -> Result<(), String> {
if self.available.load(Ordering::Acquire) {
Ok(())
} else {
Err("profile store is unavailable".to_owned())
}
}
fn disable<T>(&self, error: impl Into<String>) -> Result<T, String> {
self.available.store(false, Ordering::Release);
Err(error.into())
}
}
enum ValidationIssue {
Invalid,
Fatal(String),
}
fn validate_snapshot(ordering: &K1TxnOrdering, snapshot: &Snapshot) -> Result<(), ValidationIssue> {
if let Some(cursor) = snapshot.cursor() {
validate_transaction(ordering, cursor)?;
}
for profile in snapshot.profiles() {
validate_transaction(ordering, profile.profile_id().txid())?;
validate_transaction(ordering, profile.revision().txid())?;
}
Ok(())
}
fn validate_transaction(ordering: &K1TxnOrdering, id: TxId) -> Result<(), ValidationIssue> {
let bytes = ordering
.get_txn(id)
.map_err(|error| ValidationIssue::Fatal(format!("KTO query failed: {error}")))?
.ok_or(ValidationIssue::Invalid)?;
if !id.verify(&bytes) {
return Err(ValidationIssue::Invalid);
}
let transaction = Transaction::parse(&bytes).map_err(|_| ValidationIssue::Invalid)?;
if transaction.subsystem().as_str() != SUBSYSTEM {
return Err(ValidationIssue::Invalid);
}
Ok(())
}