560 lines
26 KiB
Rust
560 lines
26 KiB
Rust
// file: crates/ksp-wallet-lib/src/persistence.rs
|
|
// version: 8
|
|
|
|
//! Async-first native Wallet V1/V2 filesystem persistence and version-neutral dispatch.
|
|
|
|
use std::io::Read; // rust-rules: trait-import
|
|
use std::io::Write; // rust-rules: trait-import
|
|
|
|
/// Creates a new native `.kspwallet` using [`crate::DEFAULT_WALLET_FORMAT`].
|
|
///
|
|
/// The default is explicitly V2 in this release and does not track future `LATEST_SUPPORTED_WALLET_FORMAT` values automatically.
|
|
pub async fn create_wallet_file(
|
|
destination: impl std::convert::AsRef<std::path::Path>,
|
|
owner_password: crate::OwnerPassword,
|
|
view_password: std::option::Option<crate::ViewPassword>,
|
|
metadata: crate::WalletCreateMetadata,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
return match crate::DEFAULT_WALLET_FORMAT {
|
|
crate::WalletFormat::V1 => create_wallet_file_v1(destination, owner_password, view_password, metadata).await,
|
|
crate::WalletFormat::V2 => create_wallet_file_v2(destination, owner_password, view_password, metadata).await,
|
|
};
|
|
}
|
|
|
|
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
|
|
pub async fn create_wallet_file_v2(
|
|
destination: impl std::convert::AsRef<std::path::Path>,
|
|
owner_password: crate::OwnerPassword,
|
|
view_password: std::option::Option<crate::ViewPassword>,
|
|
metadata: crate::WalletCreateMetadata,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let destination = destination.as_ref().to_path_buf();
|
|
let owner = match crate::create_wallet_v2(owner_password, view_password, metadata).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let serialized = match owner.to_native_bytes() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = persist_new_wallet_async(destination, serialized).await {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_create_file",
|
|
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
|
"native wallet persisted with no-clobber semantics"
|
|
);
|
|
return std::result::Result::Ok(owner);
|
|
}
|
|
|
|
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
|
|
///
|
|
/// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then
|
|
/// published with no-clobber semantics. The destination directory must already exist; Wallet never discovers or creates a configured default directory.
|
|
pub async fn create_wallet_file_v1(
|
|
destination: impl std::convert::AsRef<std::path::Path>,
|
|
owner_password: crate::OwnerPassword,
|
|
view_password: std::option::Option<crate::ViewPassword>,
|
|
metadata: crate::WalletCreateMetadataV1,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let destination = destination.as_ref().to_path_buf();
|
|
let owner_result = crate::create_wallet_v1(owner_password, view_password, metadata).await;
|
|
let owner = match owner_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let serialized_result = owner.to_json_bytes();
|
|
let serialized = match serialized_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let persist_result = persist_new_wallet_async(destination, serialized).await;
|
|
if let std::result::Result::Err(error) = persist_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_create_file",
|
|
format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
|
"native wallet persisted with no-clobber semantics"
|
|
);
|
|
return std::result::Result::Ok(owner);
|
|
}
|
|
|
|
/// Opens a supported native `.kspwallet` file with VIEW capability after bounded V1/V2 detection.
|
|
pub async fn open_wallet_view_file(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::ViewPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletView> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_view(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
|
|
pub async fn open_wallet_view_file_v2(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::ViewPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletView> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_view_v2(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Opens a native `.kspwallet` V1 from `source` with VIEW capability.
|
|
///
|
|
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow.
|
|
pub async fn open_wallet_view_file_v1(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::ViewPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletView> {
|
|
let source = source.as_ref().to_path_buf();
|
|
let bytes_result = read_wallet_file_async(source).await;
|
|
let bytes = match bytes_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_view_v1(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Opens a supported native `.kspwallet` file with OWNER capability after bounded V1/V2 detection.
|
|
pub async fn open_wallet_owner_file(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::OwnerPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_owner(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
|
|
pub async fn open_wallet_owner_file_v2(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::OwnerPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_owner_v2(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Opens a native `.kspwallet` V1 from `source` with OWNER capability.
|
|
///
|
|
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow.
|
|
pub async fn open_wallet_owner_file_v1(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
password: crate::OwnerPassword,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let source = source.as_ref().to_path_buf();
|
|
let bytes_result = read_wallet_file_async(source).await;
|
|
let bytes = match bytes_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::open_wallet_owner_v1(bytes.as_slice(), password).await;
|
|
}
|
|
|
|
/// Reads and verifies the locked projection of a supported native `.kspwallet` file after bounded V1/V2 detection.
|
|
pub async fn inspect_locked_wallet_file(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::inspect_locked_wallet(bytes.as_slice());
|
|
}
|
|
|
|
/// Reads and verifies the locked projection of a native `.kspwallet` V2 binary file without running a password KDF.
|
|
pub async fn inspect_locked_wallet_file_v2(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
|
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::inspect_locked_wallet_v2(bytes.as_slice());
|
|
}
|
|
|
|
/// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF.
|
|
pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
|
let source = source.as_ref().to_path_buf();
|
|
let bytes_result = read_wallet_file_async(source).await;
|
|
let bytes = match bytes_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return crate::inspect_locked_wallet_v1(bytes.as_slice());
|
|
}
|
|
|
|
/// Persists new wallet content v1.
|
|
pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
|
return persist_new_wallet_async(destination, content).await;
|
|
}
|
|
|
|
/// Persists one already serialized native Wallet document with no-clobber semantics.
|
|
pub(crate) async fn persist_new_wallet_content(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
|
return persist_new_wallet_async(destination, content).await;
|
|
}
|
|
|
|
/// Replaces wallet file v1.
|
|
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)),
|
|
};
|
|
}
|
|
|
|
/// Replaces one authenticated V2 Wallet file only if the current V2 state still matches the caller's expected state.
|
|
pub(crate) async fn replace_wallet_file_v2(
|
|
destination: std::path::PathBuf,
|
|
expected_current: crate::KspWalletEnvelopeV2,
|
|
content: std::vec::Vec<u8>,
|
|
) -> ksp_core_lib::Result<()> {
|
|
let task = tokio::task::spawn_blocking(move || {
|
|
return replace_wallet_file_v2_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)),
|
|
};
|
|
}
|
|
|
|
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
|
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
|
return match task.await {
|
|
std::result::Result::Ok(result) => result,
|
|
std::result::Result::Err(error) => std::result::Result::Err(blocking_io_error("read_task", error)),
|
|
};
|
|
}
|
|
|
|
async fn persist_new_wallet_async(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
|
let task = tokio::task::spawn_blocking(move || return persist_new_wallet_blocking(destination.as_path(), 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("create_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 {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(io_error("open", error)),
|
|
};
|
|
let metadata_result = file.metadata();
|
|
let metadata = match metadata_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(io_error("metadata", error)),
|
|
};
|
|
if metadata.len() > crate::KSPWALLET_MAX_FILE_BYTES as u64 {
|
|
return std::result::Result::Err(oversized_document_error());
|
|
}
|
|
let capacity = std::cmp::min(metadata.len(), crate::KSPWALLET_MAX_FILE_BYTES as u64) as usize;
|
|
let mut bytes = std::vec::Vec::with_capacity(capacity);
|
|
let mut bounded = file.take((crate::KSPWALLET_MAX_FILE_BYTES + 1) as u64);
|
|
let read_result = bounded.read_to_end(&mut bytes);
|
|
if let std::result::Result::Err(error) = read_result {
|
|
return std::result::Result::Err(io_error("read", error));
|
|
}
|
|
if bytes.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
|
return std::result::Result::Err(oversized_document_error());
|
|
}
|
|
return std::result::Result::Ok(bytes);
|
|
}
|
|
|
|
fn persist_new_wallet_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
|
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_v2_checked_blocking(
|
|
destination: &std::path::Path,
|
|
expected_current: &crate::KspWalletEnvelopeV2,
|
|
content: &[u8],
|
|
) -> ksp_core_lib::Result<()> {
|
|
let current_check = verify_expected_wallet_state_v2(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_v2(destination, expected_current));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
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::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 verify_expected_wallet_state_v2(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV2) -> 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::KspWalletEnvelopeV2::parse_binary(current_bytes.as_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = crate::verify_state_signature_v2(¤t) {
|
|
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<()>,
|
|
{
|
|
if content.len() > crate::KSPWALLET_MAX_FILE_BYTES {
|
|
return std::result::Result::Err(oversized_document_error());
|
|
}
|
|
let parent = destination_parent(destination);
|
|
let parent = match parent {
|
|
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 temporary_result = tempfile::Builder::new().prefix(".kspwallet-write-").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("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("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("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_noclobber(destination);
|
|
let persisted = match persist_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
if error.error.kind() == std::io::ErrorKind::AlreadyExists {
|
|
return std::result::Result::Err(destination_exists_error());
|
|
}
|
|
return std::result::Result::Err(atomic_io_error("publish_noclobber", 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("published_file_sync", error));
|
|
}
|
|
sync_parent_directory_best_effort(parent);
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn destination_parent(destination: &std::path::Path) -> ksp_core_lib::Result<&std::path::Path> {
|
|
return match destination.parent() {
|
|
std::option::Option::Some(parent) if !parent.as_os_str().is_empty() => std::result::Result::Ok(parent),
|
|
std::option::Option::Some(_) => std::result::Result::Ok(std::path::Path::new(".")),
|
|
std::option::Option::None => std::result::Result::Err(atomic_error("destination", "Wallet destination has no parent directory")),
|
|
};
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn sync_parent_directory_best_effort(parent: &std::path::Path) {
|
|
let opened = std::fs::File::open(parent);
|
|
let directory = match opened {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
ksp_logging_lib::warn!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_parent_directory_sync",
|
|
io_kind = ?error.kind(),
|
|
"wallet publication succeeded but parent-directory durability sync could not start"
|
|
);
|
|
return;
|
|
},
|
|
};
|
|
if let std::result::Result::Err(error) = directory.sync_all() {
|
|
ksp_logging_lib::warn!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_parent_directory_sync",
|
|
io_kind = ?error.kind(),
|
|
"wallet publication succeeded but parent-directory durability sync failed"
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn sync_parent_directory_best_effort(_parent: &std::path::Path) {
|
|
return;
|
|
}
|
|
|
|
fn oversized_document_error() -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet document exceeds the V1 size limit").with_context("field", "document");
|
|
}
|
|
|
|
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)
|
|
.with_source(source);
|
|
}
|
|
|
|
fn atomic_error(operation: &'static str, message: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, message).with_context("operation", operation);
|
|
}
|
|
|
|
fn atomic_io_error(operation: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, "Atomic Wallet persistence failed")
|
|
.with_context("operation", operation)
|
|
.with_source(source);
|
|
}
|
|
|
|
fn blocking_io_error(operation: &'static str, source: tokio::task::JoinError) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_IO_FAILED, "Wallet filesystem task failed")
|
|
.with_context("operation", operation)
|
|
.with_source(source);
|
|
}
|
|
|
|
fn blocking_atomic_error(operation: &'static str, source: tokio::task::JoinError) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED, "Atomic Wallet persistence task failed")
|
|
.with_context("operation", operation)
|
|
.with_source(source);
|
|
}
|
|
|
|
/// Persists new wallet fault before publish.
|
|
#[cfg(test)]
|
|
pub(crate) fn persist_new_wallet_fault_before_publish(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
|
return persist_new_wallet_with_hook(destination, content, || {
|
|
return std::result::Result::Err(atomic_error("fault_injection", "Injected Wallet persistence failure before publication"));
|
|
});
|
|
}
|
|
|
|
/// Persists new wallet for test.
|
|
#[cfg(test)]
|
|
pub(crate) fn persist_new_wallet_for_test(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
|
return persist_new_wallet_blocking(destination, content);
|
|
}
|
|
|
|
/// Replaces wallet fault before publish.
|
|
#[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"));
|
|
});
|
|
}
|
|
|
|
/// Replaces wallet for test.
|
|
#[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;
|