use alloc::string::String;
use crate::bytes::Bytes;
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct EnvironmentId {
pub storage: u64,
pub name: String,
pub location: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct EnvironmentBundle {
pub bytes: Bytes,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum EnvironmentOrigin {
Empty,
Fork(EnvironmentId),
Bundle(EnvironmentBundle),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum EnvironmentError {
IncompatibleStorage {
name: String,
},
Active {
name: String,
},
SelfFork {
name: String,
},
Storage {
reason: String,
},
}
impl core::fmt::Display for EnvironmentError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::IncompatibleStorage { name } => write!(
f,
"the environment {name} lives in another process's storage"
),
Self::Active { name } => {
write!(
f,
"the environment {name} is active, and cannot be replaced"
)
}
Self::SelfFork { name } => {
write!(f, "the environment {name} cannot be forked into itself")
}
Self::Storage { reason } => write!(f, "the environment storage failed: {reason}"),
}
}
}
impl core::error::Error for EnvironmentError {}
#[cfg(native_cache)]
mod storage {
use super::*;
use crate::bundle::{BundleFormat, ExportOptions, SqliteBundle, export, import_into};
use crate::environment::{activate, active, file_name, load, path, root, sanitize};
use crate::persistence::database::same_file;
use crate::persistence::{Access, Database, DatabaseStorage, Storage};
use crate::sync::LazyLock;
use alloc::boxed::Box;
use alloc::string::ToString;
use std::path::{Path, PathBuf};
static STORAGE: LazyLock<u64> = LazyLock::new(crate::rand::gen_random);
impl EnvironmentId {
pub fn active() -> Self {
let location = path();
let named = active();
let name = match location == root().join(file_name(&named)) {
true => named.to_string(),
false => location
.file_stem()
.map_or_else(|| named.to_string(), |stem| stem.to_string_lossy().into()),
};
Self::local(name, &location)
}
pub fn create(name: &str, origin: EnvironmentOrigin) -> Result<Self, EnvironmentError> {
let name = sanitize(name);
let id = Self::local(name.clone(), &root().join(file_name(&name)));
if same_file(&path(), &id.path()) {
return Err(EnvironmentError::Active { name: id.name });
}
let incoming = Incoming::new(&id, origin)?;
if let Incoming::Itself = incoming {
return Err(EnvironmentError::SelfFork { name: id.name });
}
std::fs::create_dir_all(root()).map_err(storage_error)?;
let scratch = ScratchFile::new(&id.path());
let source = incoming.open(&scratch)?;
id.open(Access::ReadWrite)?
.replace_with(source.as_ref())
.map_err(|reason| EnvironmentError::Storage { reason })?;
Ok(id)
}
pub fn activate(&self) -> Result<Self, EnvironmentError> {
self.check_local()?;
let previous = Self::active();
match self.path() == root().join(file_name(&self.name)) {
true => activate(&self.name),
false => load(self.path()),
}
Ok(previous)
}
pub fn merge(&self, origin: EnvironmentOrigin) -> Result<(), EnvironmentError> {
self.check_local()?;
let scratch = ScratchFile::new(&self.path());
let Some(source) = Incoming::new(self, origin)?.open(&scratch)? else {
return Ok(());
};
let path = self.path();
let open = |namespace: &str| -> Result<Box<dyn Storage>, EnvironmentError> {
let database = Database::open_at(&path, Access::ReadWrite)
.map_err(|reason| EnvironmentError::Storage { reason })?;
Ok(Box::new(DatabaseStorage::new(
database,
namespace.to_string(),
)))
};
match import_into(&source, open)?.failed {
0 => Ok(()),
failed => Err(EnvironmentError::Storage {
reason: alloc::format!("{failed} entries were refused"),
}),
}
}
pub fn save(&self) -> Result<EnvironmentBundle, EnvironmentError> {
self.check_local()?;
let scratch = ScratchFile::new(&self.path());
let options = ExportOptions {
name: self.name.clone(),
format: BundleFormat::Sqlite,
..Default::default()
};
export(&[self.path()], scratch.path(), &options).map_err(storage_error)?;
let saved = std::fs::read(scratch.path()).map_err(storage_error)?;
Ok(EnvironmentBundle {
bytes: Bytes::from_bytes_vec(saved),
})
}
fn local(name: String, location: &Path) -> Self {
Self {
storage: *STORAGE,
name,
location: location.display().to_string(),
}
}
fn check_local(&self) -> Result<(), EnvironmentError> {
match self.storage == *STORAGE {
true => Ok(()),
false => Err(EnvironmentError::IncompatibleStorage {
name: self.name.clone(),
}),
}
}
fn open(&self, access: Access) -> Result<Database, EnvironmentError> {
Database::open_at(&self.path(), access)
.map_err(|reason| EnvironmentError::Storage { reason })
}
fn path(&self) -> PathBuf {
PathBuf::from(&self.location)
}
}
enum Incoming {
Nothing,
Itself,
Fork(EnvironmentId),
Bundle(EnvironmentBundle),
}
impl Incoming {
fn new(
target: &EnvironmentId,
origin: EnvironmentOrigin,
) -> Result<Self, EnvironmentError> {
Ok(match origin {
EnvironmentOrigin::Empty => Self::Nothing,
EnvironmentOrigin::Fork(source) => {
source.check_local()?;
match same_file(&source.path(), &target.path()) {
true => Self::Itself,
false => Self::Fork(source),
}
}
EnvironmentOrigin::Bundle(bundle) => Self::Bundle(bundle),
})
}
fn open(self, scratch: &ScratchFile) -> Result<Option<Database>, EnvironmentError> {
Ok(match self {
Self::Nothing | Self::Itself => None,
Self::Fork(source) => Some(source.open(Access::ReadOnly)?),
Self::Bundle(bundle) => {
std::fs::write(scratch.path(), &*bundle.bytes).map_err(storage_error)?;
drop(SqliteBundle::open(scratch.path()).map_err(storage_error)?);
Some(
Database::open_at(scratch.path(), Access::ReadOnly)
.map_err(|reason| EnvironmentError::Storage { reason })?,
)
}
})
}
}
struct ScratchFile {
path: PathBuf,
}
impl ScratchFile {
fn new(beside: &Path) -> Self {
let mut path = beside.to_path_buf();
let suffix: u64 = crate::rand::gen_random();
path.set_extension(alloc::format!("{suffix:016x}.scratch"));
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for ScratchFile {
fn drop(&mut self) {
for suffix in ["", "-wal", "-shm", "-tshm"] {
let mut file = std::ffi::OsString::from(&self.path);
file.push(suffix);
let _ = std::fs::remove_file(file);
}
}
}
fn storage_error(error: impl ToString) -> EnvironmentError {
EnvironmentError::Storage {
reason: error.to_string(),
}
}
}