v0.2.5-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-wallet-lib/src/persistence.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Async-first native Wallet V1 filesystem persistence.
|
||||
|
||||
@@ -98,6 +98,20 @@ async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std:
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) async fn replace_wallet_file_v1(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: crate::KspWalletEnvelopeV1,
|
||||
content: std::vec::Vec<u8>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
return replace_wallet_file_checked_blocking(destination.as_path(), &expected_current, content.as_slice());
|
||||
});
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("replace_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
fn read_wallet_file_blocking(source: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let opened = std::fs::File::open(source);
|
||||
let file = match opened {
|
||||
@@ -130,6 +144,95 @@ fn persist_new_wallet_blocking(destination: &std::path::Path, content: &[u8]) ->
|
||||
return persist_new_wallet_with_hook(destination, content, || return std::result::Result::Ok(()));
|
||||
}
|
||||
|
||||
fn replace_wallet_file_checked_blocking(
|
||||
destination: &std::path::Path,
|
||||
expected_current: &crate::KspWalletEnvelopeV1,
|
||||
content: &[u8],
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let current_check = verify_expected_wallet_state(destination, expected_current);
|
||||
if let std::result::Result::Err(error) = current_check {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current));
|
||||
}
|
||||
|
||||
fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(()));
|
||||
}
|
||||
|
||||
fn verify_expected_wallet_state(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV1) -> ksp_core_lib::Result<()> {
|
||||
let current_bytes = match read_wallet_file_blocking(destination) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let current = match crate::KspWalletEnvelopeV1::parse_json(current_bytes.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let verify_result = crate::wallet::verify_state_signature(¤t);
|
||||
if let std::result::Result::Err(error) = verify_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if ¤t != expected_current {
|
||||
return std::result::Result::Err(state_conflict_error());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
|
||||
where
|
||||
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
||||
{
|
||||
if content.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
||||
return std::result::Result::Err(oversized_document_error());
|
||||
}
|
||||
let parent = match destination_parent(destination) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if destination.file_name().is_none() {
|
||||
return std::result::Result::Err(atomic_error("destination", "Wallet destination has no file name"));
|
||||
}
|
||||
let existing_metadata = std::fs::metadata(destination);
|
||||
let existing_metadata = match existing_metadata {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(io_error("replace_metadata", error)),
|
||||
};
|
||||
if !existing_metadata.is_file() {
|
||||
return std::result::Result::Err(atomic_error("replace_destination", "Wallet replacement destination is not a regular file"));
|
||||
}
|
||||
|
||||
let temporary_result = tempfile::Builder::new().prefix(".kspwallet-replace-").suffix(".tmp").tempfile_in(parent);
|
||||
let mut temporary = match temporary_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(atomic_io_error("replacement_temporary_create", error)),
|
||||
};
|
||||
let write_result = temporary.write_all(content);
|
||||
if let std::result::Result::Err(error) = write_result {
|
||||
return std::result::Result::Err(atomic_io_error("replacement_temporary_write", error));
|
||||
}
|
||||
let sync_result = temporary.as_file().sync_all();
|
||||
if let std::result::Result::Err(error) = sync_result {
|
||||
return std::result::Result::Err(atomic_io_error("replacement_temporary_sync", error));
|
||||
}
|
||||
let hook_result = before_publish();
|
||||
if let std::result::Result::Err(error) = hook_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
|
||||
let persist_result = temporary.persist(destination);
|
||||
let persisted = match persist_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(atomic_io_error("replacement_publish", error.error)),
|
||||
};
|
||||
let final_sync_result = persisted.sync_all();
|
||||
if let std::result::Result::Err(error) = final_sync_result {
|
||||
return std::result::Result::Err(atomic_io_error("replacement_file_sync", error));
|
||||
}
|
||||
sync_parent_directory_best_effort(parent);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn persist_new_wallet_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
|
||||
where
|
||||
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
||||
@@ -230,6 +333,10 @@ fn destination_exists_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_DESTINATION_EXISTS, "Wallet destination already exists");
|
||||
}
|
||||
|
||||
fn state_conflict_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_STATE_CONFLICT, "Wallet replacement source state does not match the authenticated handle");
|
||||
}
|
||||
|
||||
fn io_error(operation: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_IO_FAILED, "Wallet filesystem I/O failed")
|
||||
.with_context("operation", operation)
|
||||
@@ -270,6 +377,18 @@ pub(crate) fn persist_new_wallet_for_test(destination: &std::path::Path, content
|
||||
return persist_new_wallet_blocking(destination, content);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn replace_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return replace_wallet_file_with_hook(destination, content, || {
|
||||
return std::result::Result::Err(atomic_error("replace_fault_injection", "Injected Wallet replacement failure before publication"));
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn replace_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return replace_wallet_file_blocking(destination, content);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/persistence.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user