v0.2.6-pre.018

This commit is contained in:
2026-08-22 10:56:26 +02:00
parent 362123f357
commit ba8f42f9b1
36 changed files with 1238 additions and 150 deletions

View File

@@ -1,5 +1,5 @@
# file: crates/ksp-config-lib/Cargo.toml
# version: 6
# version: 7
[package]
name = "ksp-config-lib"
@@ -8,6 +8,7 @@ edition.workspace = true
repository.workspace = true
[dependencies]
directories.workspace = true
ksp-core-lib = { path = "../ksp-core-lib" }
ksp-logging-lib = { path = "../ksp-logging-lib" }
ksp-onchain-transport-lib = { path = "../ksp-onchain-transport-lib" }
@@ -16,6 +17,7 @@ serde_json.workspace = true
jsonschema.workspace = true
[dev-dependencies]
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt"] }
[lints]

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/error.rs
// version: 9
// version: 10
/// Error code used when a Config bootstrap argument is missing its value.
pub const ERROR_CODE_BOOTSTRAP_ARGUMENT_MISSING_VALUE: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "bootstrap_argument_missing_value");
@@ -37,6 +37,9 @@ pub const ERROR_CODE_JSON_FILE_READ_FAILED: ksp_core_lib::ErrorCode = ksp_core_l
pub const ERROR_CODE_JSON_SYNTAX_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "json_syntax_invalid");
/// Error code used when an explicit Config management operation is unsupported or targets the wrong managed resource kind.
pub const ERROR_CODE_MANAGEMENT_OPERATION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "management_operation_invalid");
/// Error code used when packaged desktop resources cannot initialize the shared writable KSP runtime layout.
pub const ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("config", "packaged_runtime_preparation_failed");
/// Error code used when an atomic managed Config or `.env` persistence operation fails before commit.
pub const ERROR_CODE_PERSISTENCE_WRITE_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("config", "persistence_write_failed");
/// Error code used when an explicitly requested Config profile does not exist in a validated document.

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/src/lib.rs
// version: 15
// version: 16
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -20,6 +20,7 @@ mod environment;
mod error;
mod logging;
mod management;
mod packaging;
mod persistence;
mod profile;
mod registry;
@@ -91,6 +92,8 @@ pub use self::error::ERROR_CODE_JSON_FILE_READ_FAILED;
pub use self::error::ERROR_CODE_JSON_SYNTAX_INVALID;
/// Error code used when an explicit management operation is unsupported or targets the wrong managed resource kind.
pub use self::error::ERROR_CODE_MANAGEMENT_OPERATION_INVALID;
/// Error code used when packaged desktop resources cannot initialize the shared writable KSP runtime layout.
pub use self::error::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED;
/// Error code used when atomic managed Config or `.env` persistence fails before commit.
pub use self::error::ERROR_CODE_PERSISTENCE_WRITE_FAILED;
/// Error code used when an explicitly requested Config profile does not exist.
@@ -123,6 +126,10 @@ pub use self::management::LoggingOutputFilterConfig;
pub use self::management::LoggingProfileConfig;
/// Typed source contract for one global Logging target override.
pub use self::management::LoggingTargetFilterConfig;
/// Writable KSP runtime roots prepared from packaged Config resources.
pub use self::packaging::PackagedRuntimeLayout;
/// Prepares the shared writable KSP desktop runtime from immutable packaged resources.
pub use self::packaging::prepare_packaged_runtime;
/// Source that selected an effective standard Config profile.
pub use self::profile::ConfigProfileSelectionSource;
/// Origin of one top-level value in a resolved standard Config profile.

View File

@@ -0,0 +1,167 @@
// file: crates/ksp-config-lib/src/packaging.rs
// version: 1
//! Packaged desktop runtime layout owned by Config.
const PACKAGED_PROJECT_APPLICATION: &str = "khadhroony-solana-project";
const PACKAGED_PROJECT_ORGANIZATION: &str = "SASEDEV";
const PACKAGED_PROJECT_QUALIFIER: &str = "com";
/// Writable KSP runtime roots prepared from packaged Config resources.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackagedRuntimeLayout {
runtime_root: std::path::PathBuf,
cfg_path: std::path::PathBuf,
schema_path: std::path::PathBuf,
}
impl PackagedRuntimeLayout {
/// Returns the shared writable KSP runtime root used as the packaged process working directory.
#[must_use]
pub fn runtime_root(&self) -> &std::path::Path {
return self.runtime_root.as_path();
}
/// Returns the writable root containing Config-managed runtime documents.
#[must_use]
pub fn cfg_path(&self) -> &std::path::Path {
return self.cfg_path.as_path();
}
/// Returns the writable root containing the package-owned current JSON Schemas.
#[must_use]
pub fn schema_path(&self) -> &std::path::Path {
return self.schema_path.as_path();
}
}
/// Prepares the shared writable KSP desktop runtime from immutable packaged resources.
///
/// Runtime Config documents are seeded only when absent so existing user-managed sources are never silently overwritten. JSON Schemas are package-owned
/// authority and are synchronized on each packaged launch. The local `.env`, Logging outputs and Wallet roots remain ordinary relative runtime resources
/// below the returned shared root unless their managed Config explicitly selects another path.
pub fn prepare_packaged_runtime(resource_root: &std::path::Path) -> ksp_core_lib::Result<PackagedRuntimeLayout> {
let project_dirs = directories::ProjectDirs::from(PACKAGED_PROJECT_QUALIFIER, PACKAGED_PROJECT_ORGANIZATION, PACKAGED_PROJECT_APPLICATION);
let project_dirs = match project_dirs {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ksp_core_lib::Error::new(
crate::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED,
"Cannot resolve the writable packaged KSP runtime directory",
));
},
};
return prepare_packaged_runtime_at(resource_root, project_dirs.data_dir());
}
fn prepare_packaged_runtime_at(resource_root: &std::path::Path, runtime_root: &std::path::Path) -> ksp_core_lib::Result<PackagedRuntimeLayout> {
let packaged_cfg_path = resource_root.join(crate::DEFAULT_CFG_PATH);
let packaged_schema_path = resource_root.join(crate::DEFAULT_SCHEMA_PATH);
let cfg_path = runtime_root.join(crate::DEFAULT_CFG_PATH);
let schema_path = runtime_root.join(crate::DEFAULT_SCHEMA_PATH);
let runtime_creation = std::fs::create_dir_all(runtime_root);
if let std::result::Result::Err(error) = runtime_creation {
return std::result::Result::Err(packaging_io_error(runtime_root, "packaged KSP runtime root cannot be created", error));
}
let cfg_creation = std::fs::create_dir_all(cfg_path.as_path());
if let std::result::Result::Err(error) = cfg_creation {
return std::result::Result::Err(packaging_io_error(cfg_path.as_path(), "packaged Config root cannot be created", error));
}
let schema_creation = std::fs::create_dir_all(schema_path.as_path());
if let std::result::Result::Err(error) = schema_creation {
return std::result::Result::Err(packaging_io_error(schema_path.as_path(), "packaged Config schema root cannot be created", error));
}
let registry = crate::ConfigFileRegistry::defaults();
let registry = match registry {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
for descriptor in registry.descriptors() {
let (source_root, destination_root) = match descriptor.kind() {
crate::ConfigFileKind::Config => (packaged_cfg_path.as_path(), cfg_path.as_path()),
crate::ConfigFileKind::Schema => (packaged_schema_path.as_path(), schema_path.as_path()),
};
let source = source_root.join(descriptor.filename());
let destination = destination_root.join(descriptor.filename());
let content = read_packaged_resource(source.as_path());
let content = match content {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persistence = match descriptor.kind() {
crate::ConfigFileKind::Config => seed_config_document(destination.as_path(), content.as_slice()),
crate::ConfigFileKind::Schema => synchronize_schema(destination.as_path(), content.as_slice()),
};
if let std::result::Result::Err(error) = persistence {
return std::result::Result::Err(error);
}
}
return std::result::Result::Ok(PackagedRuntimeLayout { runtime_root: runtime_root.to_path_buf(), cfg_path, schema_path });
}
fn read_packaged_resource(path: &std::path::Path) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let metadata = std::fs::symlink_metadata(path);
let metadata = match metadata {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(packaging_io_error(path, "packaged Config resource metadata cannot be read", error));
},
};
if metadata.file_type().is_symlink() || !metadata.is_file() {
return std::result::Result::Err(packaging_error(path, "packaged Config resource must be a regular non-symlink file"));
}
let content = std::fs::read(path);
return match content {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(packaging_io_error(path, "packaged Config resource cannot be read", error)),
};
}
fn seed_config_document(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
let existing = destination_state(path);
let existing = match existing {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if existing {
return std::result::Result::Ok(());
}
return crate::atomic_write(path, content);
}
fn synchronize_schema(path: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
let existing = destination_state(path);
if let std::result::Result::Err(error) = existing {
return std::result::Result::Err(error);
}
return crate::atomic_write(path, content);
}
fn destination_state(path: &std::path::Path) -> ksp_core_lib::Result<bool> {
let metadata = std::fs::symlink_metadata(path);
return match metadata {
std::result::Result::Ok(value) => {
if value.file_type().is_symlink() || !value.is_file() {
std::result::Result::Err(packaging_error(path, "packaged runtime destination must be a regular non-symlink file when it exists"))
} else {
std::result::Result::Ok(true)
}
},
std::result::Result::Err(error) if error.kind() == std::io::ErrorKind::NotFound => std::result::Result::Ok(false),
std::result::Result::Err(error) => std::result::Result::Err(packaging_io_error(path, "packaged runtime destination metadata cannot be read", error)),
};
}
fn packaging_error(path: &std::path::Path, reason: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED, "Packaged KSP runtime preparation failed")
.with_context("path", path.to_string_lossy().into_owned())
.with_context("reason", reason);
}
fn packaging_io_error(path: &std::path::Path, reason: &'static str, source: std::io::Error) -> ksp_core_lib::Error {
return packaging_error(path, reason).with_source(source);
}
#[cfg(test)]
#[path = "../unit_tests/packaging.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-config-lib/tests/public_api.rs
// version: 20
// version: 21
//! Integration tests for the public `ksp-config-lib` bootstrap, registry, JSON/profile/composite, environment-resolution, sensitivity,
//! Logging/Transport adapters and management contracts.
@@ -270,3 +270,11 @@ fn wallet_adapter_contract_is_available_from_crate_root() {
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_FILENAME, "std.wallet.json");
assert_eq!(ksp_config_lib::DEFAULT_STD_WALLET_SCHEMA_FILENAME, "std.wallet.schema.json");
}
#[test]
fn packaged_runtime_layout_contract_is_available_from_crate_root() {
let prepare: fn(&std::path::Path) -> ksp_core_lib::Result<ksp_config_lib::PackagedRuntimeLayout> = ksp_config_lib::prepare_packaged_runtime;
let _ = prepare;
assert_eq!(ksp_config_lib::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED.domain(), "config");
assert_eq!(ksp_config_lib::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED.code(), "packaged_runtime_preparation_failed");
}

View File

@@ -0,0 +1,90 @@
// file: crates/ksp-config-lib/unit_tests/packaging.rs
// version: 2
fn write_packaged_registry(resource_root: &std::path::Path, marker: &str) -> std::io::Result<()> {
let registry = crate::ConfigFileRegistry::defaults();
assert!(registry.is_ok(), "Config registry fixture should be constructible: {registry:?}");
if let std::result::Result::Ok(registry) = registry {
for descriptor in registry.descriptors() {
let root = match descriptor.kind() {
crate::ConfigFileKind::Config => resource_root.join(crate::DEFAULT_CFG_PATH),
crate::ConfigFileKind::Schema => resource_root.join(crate::DEFAULT_SCHEMA_PATH),
};
let create = std::fs::create_dir_all(root.as_path());
if let std::result::Result::Err(error) = create {
return std::result::Result::Err(error);
}
let path = root.join(descriptor.filename());
let content = format!("{marker}:{}", descriptor.file_id().as_str());
let write = std::fs::write(path, content.as_bytes());
if let std::result::Result::Err(error) = write {
return std::result::Result::Err(error);
}
}
}
return std::result::Result::Ok(());
}
#[test]
fn packaged_runtime_seeds_configs_and_synchronizes_schemas_without_overwriting_user_config() {
let resource = tempfile::tempdir();
assert!(resource.is_ok(), "resource fixture should be creatable: {resource:?}");
let runtime = tempfile::tempdir();
assert!(runtime.is_ok(), "runtime fixture should be creatable: {runtime:?}");
if let (std::result::Result::Ok(resource), std::result::Result::Ok(runtime)) = (resource, runtime) {
let packaged = write_packaged_registry(resource.path(), "first");
assert!(packaged.is_ok(), "packaged Config fixture should be writable: {packaged:?}");
let first = super::prepare_packaged_runtime_at(resource.path(), runtime.path());
assert!(first.is_ok(), "first packaged runtime should prepare: {first:?}");
if let std::result::Result::Ok(first) = first {
assert_eq!(first.runtime_root(), runtime.path());
assert_eq!(first.cfg_path(), runtime.path().join(crate::DEFAULT_CFG_PATH));
assert_eq!(first.schema_path(), runtime.path().join(crate::DEFAULT_SCHEMA_PATH));
let user_logging = first.cfg_path().join(crate::DEFAULT_STD_LOGGING_FILENAME);
let user_write = std::fs::write(user_logging.as_path(), b"user-owned-config");
assert!(user_write.is_ok(), "Config fixture should be replaceable: {user_write:?}");
let packaged = write_packaged_registry(resource.path(), "second");
assert!(packaged.is_ok(), "updated packaged fixture should be writable: {packaged:?}");
let second = super::prepare_packaged_runtime_at(resource.path(), runtime.path());
assert!(second.is_ok(), "second packaged runtime should prepare: {second:?}");
let retained = std::fs::read(user_logging.as_path());
assert!(retained.is_ok(), "retained Config fixture should remain readable: {retained:?}");
if let std::result::Result::Ok(retained) = retained {
assert_eq!(retained, b"user-owned-config");
}
let schema = first.schema_path().join(crate::DEFAULT_STD_LOGGING_SCHEMA_FILENAME);
let synchronized = std::fs::read_to_string(schema.as_path());
assert!(synchronized.is_ok(), "synchronized schema fixture should be readable: {synchronized:?}");
if let std::result::Result::Ok(synchronized) = synchronized {
assert!(synchronized.starts_with("second:"));
}
}
}
}
#[cfg(unix)]
#[test]
fn packaged_runtime_rejects_symlink_destination() {
let resource = tempfile::tempdir();
assert!(resource.is_ok(), "resource fixture should be creatable: {resource:?}");
let runtime = tempfile::tempdir();
assert!(runtime.is_ok(), "runtime fixture should be creatable: {runtime:?}");
if let (std::result::Result::Ok(resource), std::result::Result::Ok(runtime)) = (resource, runtime) {
let packaged = write_packaged_registry(resource.path(), "packaged");
assert!(packaged.is_ok(), "packaged Config fixture should be writable: {packaged:?}");
let cfg = runtime.path().join(crate::DEFAULT_CFG_PATH);
let create = std::fs::create_dir_all(cfg.as_path());
assert!(create.is_ok(), "runtime Config root should be creatable: {create:?}");
let outside = runtime.path().join("outside.json");
let outside_write = std::fs::write(outside.as_path(), b"outside");
assert!(outside_write.is_ok(), "symlink target should be creatable: {outside_write:?}");
let link = cfg.join(crate::DEFAULT_STD_LOGGING_FILENAME);
let link_result = std::os::unix::fs::symlink(outside.as_path(), link.as_path());
assert!(link_result.is_ok(), "symlink fixture should be creatable: {link_result:?}");
let result = super::prepare_packaged_runtime_at(resource.path(), runtime.path());
assert!(result.is_err(), "symlink destination must be rejected");
if let std::result::Result::Err(error) = result {
assert_eq!(error.code(), crate::ERROR_CODE_PACKAGED_RUNTIME_PREPARATION_FAILED);
}
}
}