v0.2.6-pre.018
This commit is contained in:
@@ -139,6 +139,12 @@ Si le document Logging, sa résolution environnementale ou l'initialisation du r
|
||||
|
||||
Le `LoggingGuard` est conservé dans `AppState` et sert désormais au hot reload transactionnel du runtime Logging après sauvegarde.
|
||||
|
||||
## Runtime packagé
|
||||
|
||||
Le parcours release ne dépend pas du checkout source ni du répertoire depuis lequel le binaire est lancé. Les documents Config et schemas enregistrés sont embarqués comme resources Tauri. Avant `AppState::initialize`, `tauri.rs` résout le répertoire de resources puis délègue à `ksp-config-lib::prepare_packaged_runtime`. Config résout un répertoire de données utilisateur KSP writable via `ProjectDirs`, y seed les documents Config absents et y synchronise les schemas possédés par le package courant. Le process adopte ensuite ce répertoire writable comme current working directory.
|
||||
|
||||
Les documents Config déjà présents restent user-managed et ne sont jamais écrasés silencieusement lors d'une mise à jour. Les schemas sont resynchronisés à chaque lancement packagé. Le `.env` n'est jamais embarqué ni seedé ; les chemins relatifs `logs/` et `wallets/` restent ancrés dans ce runtime partagé sauf override Config. Config Desk et Wallet Desk utilisent le même identifiant de projet KSP afin de partager cette racine runtime sans dupliquer l'ownership des ressources.
|
||||
|
||||
## Lifecycle des fenêtres
|
||||
|
||||
`tw_splash.rs` valide l'origine de `splash_frontend_ready`, ignore les readiness dupliquées et orchestre le passage à `main`. Les timings sont résolus par `ksp-config-lib` depuis :
|
||||
|
||||
@@ -101,6 +101,12 @@ Les bindings sont générés au premier DTO Tauri réel ; aucune structure facti
|
||||
|
||||
Les arguments `--cfgpath`, `--schemapath` et `--filemap=...` sont transmis tels quels à `ksp-config-lib`. En build debug, le launcher replace le current working directory Rust à la racine du workspace avant ce bootstrap afin que les defaults relatifs `config/`, `config/schemas/` et `.env` désignent les ressources racine même lorsque Tauri lance `cargo run` depuis la crate de l'application. Le launcher ne lit pas ces ressources lui-même. Le profil Logging initial est le `default_profile` de `std.logging.json`.
|
||||
|
||||
### Runtime packagé
|
||||
|
||||
En release, le CWD de développement n'est pas réutilisé. Les resources Tauri embarquent les documents Config et schemas enregistrés ; `ksp-config-lib::prepare_packaged_runtime` prépare une racine de données utilisateur KSP writable résolue par `ProjectDirs`, seed les Config uniquement lorsqu'elles sont absentes et synchronise les schemas depuis le package courant. `tauri.rs` active ensuite cette racine comme current working directory avant le bootstrap Config.
|
||||
|
||||
Le `.env` n'est pas inclus dans le bundle. Les Config existantes restent intactes lors d'une mise à jour et les chemins relatifs de logs/wallets sont résolus depuis la racine runtime writable, sauf configuration explicite contraire. Le chemin physique dépend de la plateforme et ne doit pas être codé en dur par l'application ou l'utilisateur.
|
||||
|
||||
Si cette configuration ne peut pas être utilisée, l'application doit rester démarrable pour permettre sa réparation : elle utilise alors un fallback Logging console/stderr en mémoire. Le fallback n'écrit aucun fichier de configuration et n'écrase aucune valeur utilisateur.
|
||||
|
||||
La commande Tauri `get_app_snapshot` expose un état sûr du bootstrap. Ses types TypeScript sont générés par `cargo test -p ksp-app-config-desk` sous `frontend/ts/bindings/`.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-config-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.14.fix.1",
|
||||
"version": "0.2.6-pre.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// file: crates/ksp-app-config-desk/src/tauri.rs
|
||||
// version: 16
|
||||
// version: 17
|
||||
|
||||
//! Tauri runtime assembly for the KSP configuration desktop application.
|
||||
|
||||
/// Runs the configuration desktop application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
let context = tauri::generate_context!();
|
||||
let runtime_layout = configure_packaged_runtime(&context);
|
||||
if let std::result::Result::Err(error) = runtime_layout {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let app_state = crate::AppState::initialize(arguments);
|
||||
let app_state = match app_state {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -16,7 +21,7 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
builder = configure_plugins(builder);
|
||||
builder = configure_commands(builder);
|
||||
builder = configure_setup(builder);
|
||||
let run_result = builder.run(tauri::generate_context!());
|
||||
let run_result = builder.run(context);
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
@@ -26,6 +31,36 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
|
||||
if cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
|
||||
let resource_root = match resource_root {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
|
||||
let layout = match layout {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let working_directory = std::env::set_current_dir(layout.runtime_root());
|
||||
return match working_directory {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
|
||||
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.manage(app_state);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Config Desk",
|
||||
"version": "0.2.6-pre.14.fix.1",
|
||||
"version": "0.2.6-pre.18",
|
||||
"identifier": "com.sasedev.ksp-app-config-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
@@ -51,6 +51,16 @@
|
||||
"icon": [
|
||||
"icons/favicon.png",
|
||||
"icons/favicon.ico"
|
||||
]
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/tests/desktop_contract.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Desktop build/shell contract audits for Config Desk.
|
||||
|
||||
@@ -99,3 +99,24 @@ fn pre_014_template_uses_sidebar_navigation_and_kbot_style_splash_contract() {
|
||||
assert!(splash_ts.contains("add_debug"));
|
||||
assert!(splash_ts.contains("add_message"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_018_packaged_runtime_bundles_config_resources_and_activates_shared_writable_root() {
|
||||
let root = app_root();
|
||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||
assert_eq!(tauri.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.6-pre.18"));
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 8);
|
||||
assert_eq!(resources.get("../../config/std.logging.json").and_then(serde_json::Value::as_str), std::option::Option::Some("config/std.logging.json"));
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/std.wallet.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/std.wallet.schema.json"),
|
||||
);
|
||||
}
|
||||
let tauri_source = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri_source.contains("ksp_config_lib::prepare_packaged_runtime"));
|
||||
assert!(tauri_source.contains("tauri::utils::platform::resource_dir"));
|
||||
assert!(tauri_source.contains("std::env::set_current_dir(layout.runtime_root())"));
|
||||
}
|
||||
|
||||
136
crates/ksp-app-wallet-desk/README.md
Normal file
136
crates/ksp-app-wallet-desk/README.md
Normal file
@@ -0,0 +1,136 @@
|
||||
<!-- file: crates/ksp-app-wallet-desk/README.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# `ksp-app-wallet-desk`
|
||||
|
||||
`ksp-app-wallet-desk` est l'application desktop spécialisée d'administration et de validation des wallets KSP.
|
||||
|
||||
La crate est un package Tauri mixte :
|
||||
|
||||
```text
|
||||
package : ksp-app-wallet-desk
|
||||
lib : ksp_app_wallet_desk_lib
|
||||
bin : ksp-app-wallet-desk
|
||||
```
|
||||
|
||||
## Responsabilités
|
||||
|
||||
Wallet Desk reste une couche d'interface et de composition. Il ne possède ni le wire `.kspwallet`, ni la cryptographie Wallet, ni la résolution Config, ni le transport Solana :
|
||||
|
||||
```text
|
||||
ksp-config-lib -> configuration/composite/.env
|
||||
ksp-wallet-lib -> format, crypto, VIEW/OWNER, import/export, migration
|
||||
ksp-onchain-transport-lib -> RPC Solana HTTP
|
||||
ksp-logging-lib -> logging/tracing applicatif
|
||||
ksp-app-wallet-desk -> orchestration Tauri + projections sûres
|
||||
```
|
||||
|
||||
Le frontend ne reçoit jamais les keypairs, ciphertexts, passwords Config, paths complets import/export ou handles VIEW/OWNER. Les opérations privilégiées sont exécutées côté Rust.
|
||||
|
||||
## `.kspwallet` V1/V2
|
||||
|
||||
La release `0.2.6` conserve V1 et ajoute V2 :
|
||||
|
||||
```text
|
||||
V1 : JSON UTF-8 historique, lecture explicite toujours supportée
|
||||
V2 : wire binaire KSP, format de création/import par défaut
|
||||
```
|
||||
|
||||
Wallet Desk consomme uniquement les APIs non versionnées de `ksp-wallet-lib` :
|
||||
|
||||
- création/import -> format par défaut `V2` ;
|
||||
- inventory/inspection/open -> détection V1/V2 ;
|
||||
- mutations OWNER/VIEW -> restent dans le format natif ouvert ;
|
||||
- ouverture d'un V1 -> aucune migration implicite.
|
||||
|
||||
`ksp-wallet-lib` conserve parallèlement les APIs explicites `_v1` / `_v2` pour les consumers qui doivent imposer un format. `DEFAULT_WALLET_FORMAT` et `LATEST_SUPPORTED_WALLET_FORMAT` sont des politiques distinctes ; l'apparition future d'un V3 n'impose donc pas de modifier automatiquement le format créé par les APIs génériques.
|
||||
|
||||
La migration V1 -> V2 est une opération explicite OWNER-authentifiée possédée par `ksp-wallet-lib`. Wallet Desk `0.2.6` ne migre jamais silencieusement un wallet lors de sa sélection, inspection ou ouverture.
|
||||
|
||||
## Capacités fonctionnelles `0.2.6`
|
||||
|
||||
La surface validée comprend :
|
||||
|
||||
- inventory root-scoped des `.kspwallet` ;
|
||||
- create/import Solana CLI JSON et Base58 ;
|
||||
- sélection et inspection locked ;
|
||||
- ouverture VIEW et OWNER, manuelle ou via candidats secrets Config ;
|
||||
- affichage Pubkey/alias/notes uniquement après autorisation ;
|
||||
- `getBalance` via le Transport HTTP KSP ;
|
||||
- mutation alias/notes OWNER ;
|
||||
- rotations OWNER et VIEW ;
|
||||
- self-rotation VIEW ;
|
||||
- disable/recreate VIEW fort par OWNER ;
|
||||
- export OWNER Solana CLI JSON/Base58 no-clobber ;
|
||||
- wire V2 par défaut tout en conservant la compatibilité V1.
|
||||
|
||||
## Frontières desktop
|
||||
|
||||
Le frontend utilise Bootstrap, Font Awesome, DataTables/Select, SimpleBar et le bridge Logging KSP. Les capabilities Tauri du guest restent minimales :
|
||||
|
||||
```text
|
||||
core:default
|
||||
tracing:default
|
||||
```
|
||||
|
||||
Les pickers import/export sont invoqués côté Rust. Aucun accès direct filesystem ou réseau Solana n'est accordé au frontend.
|
||||
|
||||
## Développement
|
||||
|
||||
Le workspace contient plusieurs applications Tauri. `-c/--config` n'est pas un sélecteur de crate ; le cycle doit être lancé depuis le répertoire applicatif :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
En développement, le launcher Rust normalise ensuite son current working directory vers la racine du workspace avant le bootstrap Config. Les chemins relatifs `config/`, `config/schemas/`, `.env`, `logs/` et `wallets/` restent ainsi cohérents avec le checkout de développement sans déplacer leur ownership hors de `ksp-config-lib`.
|
||||
|
||||
Les ports réservés sont :
|
||||
|
||||
```text
|
||||
Vite HTTP : 1432
|
||||
Vite WS : 1433
|
||||
```
|
||||
|
||||
## Runtime packagé
|
||||
|
||||
Une application distribuée ne dépend pas du checkout source ni du répertoire depuis lequel l'utilisateur lance le binaire.
|
||||
|
||||
Les documents Config et schemas enregistrés sont embarqués comme resources Tauri. Au démarrage release :
|
||||
|
||||
1. Tauri résout son répertoire de resources ;
|
||||
2. `ksp-config-lib` résout un répertoire de données utilisateur KSP writable via `ProjectDirs` ;
|
||||
3. les documents Config packagés sont copiés uniquement lorsqu'ils sont absents ;
|
||||
4. les schemas, possédés par le package courant, sont resynchronisés à chaque lancement ;
|
||||
5. le process adopte ce répertoire KSP writable comme current working directory avant `AppState::initialize`.
|
||||
|
||||
Le `.env` n'est jamais embarqué ni prérempli. Les Config déjà modifiées par l'utilisateur ne sont jamais écrasées silencieusement. Les chemins relatifs de logs et wallets restent ancrés dans ce runtime writable, sauf override explicite par Config.
|
||||
|
||||
## Validation
|
||||
|
||||
Les validations Rust courantes sont :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-app-wallet-desk
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Le parcours fonctionnel utilise exclusivement Tauri :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Le build production de la release est exécuté seulement après tous les autres gates :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
Cette commande doit rester l'absolue dernière opération de validation de la candidate `0.2.6`.
|
||||
|
||||
Voir également [`USAGE.md`](USAGE.md), [`../../docs/formats/KSPWALLET_V1.md`](../../docs/formats/KSPWALLET_V1.md), [`../../docs/formats/KSPWALLET_V2.md`](../../docs/formats/KSPWALLET_V2.md) et [`../../docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md`](../../docs/validation/009-V0_2_6_WALLET_DESK_COMPLIANCE.md).
|
||||
140
crates/ksp-app-wallet-desk/USAGE.md
Normal file
140
crates/ksp-app-wallet-desk/USAGE.md
Normal file
@@ -0,0 +1,140 @@
|
||||
<!-- file: crates/ksp-app-wallet-desk/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de `ksp-app-wallet-desk`
|
||||
|
||||
## 1. Lancement de développement
|
||||
|
||||
Depuis la racine du workspace :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Pour isoler les wallets d'un parcours manuel :
|
||||
|
||||
```bash
|
||||
KSP_WALLETS_DIRECTORY=var/wallet-desk-manual \
|
||||
bash -lc 'cd crates/ksp-app-wallet-desk && cargo tauri dev'
|
||||
```
|
||||
|
||||
Le processus Rust debug recale son current working directory sur la racine du workspace avant le bootstrap Config. `KSP_WALLETS_DIRECTORY`, les autres variables KSP et le `.env` restent résolus exclusivement par `ksp-config-lib`.
|
||||
|
||||
## 2. Format natif
|
||||
|
||||
Wallet Desk ne sélectionne pas directement une version de wire : il utilise la façade générique de `ksp-wallet-lib`.
|
||||
|
||||
```text
|
||||
nouveau wallet -> V2
|
||||
import Solana CLI/Base58 -> V2
|
||||
inspection/ouverture -> auto-détection V1/V2
|
||||
mutation d'un V1 ouvert -> V1
|
||||
mutation d'un V2 ouvert -> V2
|
||||
```
|
||||
|
||||
La présence de V2 ne réécrit pas automatiquement les fichiers V1 existants.
|
||||
|
||||
## 3. Création / import
|
||||
|
||||
Dans **Create / Import** :
|
||||
|
||||
- la création génère une nouvelle identité Solana et un `.kspwallet` V2 ;
|
||||
- l'import accepte les formats de transfert Solana retenus, puis crée un `.kspwallet` V2 ;
|
||||
- OWNER est toujours requis ; VIEW est optionnel ;
|
||||
- les passwords sont request-only et ne sont jamais renvoyés par IPC.
|
||||
|
||||
L'import natif utilise un picker Rust. Le path source complet et les bytes de keypair ne traversent pas le frontend.
|
||||
|
||||
## 4. Inventory et ouverture
|
||||
|
||||
L'inventory n'affiche que des projections locked sûres. La sélection ne déverrouille jamais automatiquement un secret Config.
|
||||
|
||||
L'ouverture peut ensuite utiliser :
|
||||
|
||||
- un password saisi manuellement ;
|
||||
- un candidat secret Config explicitement sélectionné côté backend.
|
||||
|
||||
VIEW et OWNER produisent des handles Rust-only. La Pubkey, l'alias et les notes ne sont projetés qu'après autorisation.
|
||||
|
||||
## 5. Administration
|
||||
|
||||
OWNER permet notamment :
|
||||
|
||||
- modifier l'alias ;
|
||||
- ajouter/modifier/supprimer des notes ;
|
||||
- changer le password OWNER ;
|
||||
- changer, désactiver ou recréer VIEW ;
|
||||
- exporter la keypair en Solana CLI JSON ou Base58.
|
||||
|
||||
VIEW peut effectuer sa self-rotation lorsqu'il est actif. Les opérations destructives/privilégiées utilisent les modals Bootstrap du shell et non `window.confirm`/`alert`.
|
||||
|
||||
## 6. Migration V1 -> V2
|
||||
|
||||
La migration existe dans `ksp-wallet-lib`, mais n'est jamais un effet secondaire d'une ouverture Wallet Desk.
|
||||
|
||||
Les APIs bibliothèque dédiées sont :
|
||||
|
||||
```text
|
||||
migrate_wallet_v1_to_v2
|
||||
migrate_wallet_file_v1_to_v2
|
||||
migrate_wallet_file_v1_to_v2_in_place
|
||||
```
|
||||
|
||||
Elles authentifient OWNER, reconstruisent un document cryptographique V2 neuf et conservent l'identité Solana ainsi que les metadata/note IDs. Lorsque VIEW est activé, un password VIEW cible doit être fourni. La copie est no-clobber ; le remplacement in-place est stale-protected.
|
||||
|
||||
## 7. Balance Solana
|
||||
|
||||
Une session VIEW ou OWNER autorisée peut demander `getBalance` via `ksp-onchain-transport-lib`. Le frontend ne choisit ni Pubkey arbitraire ni URL RPC : ces valeurs sont dérivées de l'état Rust autorisé et de Config.
|
||||
|
||||
## 8. Runtime packagé
|
||||
|
||||
En build release, les Config/schemas enregistrés sont des resources du bundle Tauri. Avant le bootstrap applicatif, `ksp-config-lib` prépare un répertoire de données utilisateur commun à KSP :
|
||||
|
||||
```text
|
||||
<runtime KSP writable>/
|
||||
├── config/
|
||||
│ ├── composite.ksp-app-wallet-desk.json
|
||||
│ ├── std.logging.json
|
||||
│ ├── std.transport.json
|
||||
│ ├── std.wallet.json
|
||||
│ └── schemas/
|
||||
├── .env # créé/modifié uniquement par Config si nécessaire
|
||||
├── logs/ # par défaut si path relatif
|
||||
└── wallets/ # par défaut si path relatif
|
||||
```
|
||||
|
||||
Le chemin physique exact dépend de la plateforme et est résolu par `ProjectDirs`; l'application ne suppose aucune home directory particulière.
|
||||
|
||||
Les Config existantes sont conservées lors d'une mise à jour. Les schemas sont synchronisés depuis le package courant. Le `.env` et les secrets ne sont jamais embarqués dans le bundle.
|
||||
|
||||
## 9. Validation candidate `0.2.6`
|
||||
|
||||
Avant le build :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
cargo test -p ksp-config-lib
|
||||
cargo test -p ksp-wallet-lib
|
||||
cargo test -p ksp-app-config-desk
|
||||
cargo test -p ksp-app-wallet-desk
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Puis parcours fonctionnel :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri dev)
|
||||
```
|
||||
|
||||
Le smoke Devnet opt-in, s'il est rejoué, doit l'être avant le build final.
|
||||
|
||||
L'absolue dernière opération est :
|
||||
|
||||
```bash
|
||||
(cd crates/ksp-app-wallet-desk && cargo tauri build)
|
||||
```
|
||||
|
||||
Aucune commande de validation ne doit être exécutée après ce build pour conserver la preuve d'ordre de la candidate.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-wallet-desk/frontend/main.html -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
@@ -53,7 +53,7 @@
|
||||
<h1 id="viewTitle" class="h3 mb-1">Dashboard</h1>
|
||||
<p class="text-body-secondary mb-0">Identité autorisée et balance SOL via le Transport HTTP composite sont actives.</p>
|
||||
</div>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.14.fix.1</span>
|
||||
<span class="badge text-bg-secondary">0.2.6-pre.18</span>
|
||||
</div>
|
||||
<section data-view-panel="dashboard">
|
||||
<div class="row g-3">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ksp-app-wallet-desk",
|
||||
"private": true,
|
||||
"version": "0.2.6-pre.14.fix.1",
|
||||
"version": "0.2.6-pre.18",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/src/tauri.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
//! Tauri runtime assembly for the KSP wallet desktop application.
|
||||
|
||||
@@ -8,6 +8,11 @@ use tauri_plugin_dialog::DialogExt; // rust-rules: trait-import
|
||||
/// Runs the Wallet Desk application.
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
let context = tauri::generate_context!();
|
||||
let runtime_layout = configure_packaged_runtime(&context);
|
||||
if let std::result::Result::Err(error) = runtime_layout {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let app_state = crate::AppState::initialize(arguments);
|
||||
let app_state = match app_state {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -18,7 +23,7 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
builder = configure_plugins(builder);
|
||||
builder = configure_commands(builder);
|
||||
builder = configure_setup(builder);
|
||||
let run_result = builder.run(tauri::generate_context!());
|
||||
let run_result = builder.run(context);
|
||||
return match run_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
@@ -28,6 +33,36 @@ pub fn run(arguments: &[std::ffi::OsString]) -> ksp_core_lib::Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_packaged_runtime(context: &tauri::Context<tauri::Wry>) -> ksp_core_lib::Result<()> {
|
||||
if cfg!(debug_assertions) {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
let resource_root = tauri::utils::platform::resource_dir(context.package_info(), &tauri::Env::default());
|
||||
let resource_root = match resource_root {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot resolve packaged Tauri resource directory")
|
||||
.with_context("tauri_error", error.to_string()),
|
||||
);
|
||||
},
|
||||
};
|
||||
let layout = ksp_config_lib::prepare_packaged_runtime(resource_root.as_path());
|
||||
let layout = match layout {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let working_directory = std::env::set_current_dir(layout.runtime_root());
|
||||
return match working_directory {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(
|
||||
ksp_core_lib::Error::new(crate::ERROR_CODE_TAURI_RUNTIME_FAILED, "Cannot activate packaged KSP runtime directory")
|
||||
.with_context("runtime_root", layout.runtime_root().to_string_lossy().into_owned())
|
||||
.with_source(error),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn configure_state(builder: tauri::Builder<tauri::Wry>, app_state: crate::AppState) -> tauri::Builder<tauri::Wry> {
|
||||
return builder.manage(app_state);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "KSP Wallet Desk",
|
||||
"version": "0.2.6-pre.14.fix.1",
|
||||
"version": "0.2.6-pre.18",
|
||||
"identifier": "com.sasedev.ksp-app-wallet-desk",
|
||||
"build": {
|
||||
"beforeDevCommand": {
|
||||
@@ -51,6 +51,16 @@
|
||||
"icon": [
|
||||
"icons/favicon.png",
|
||||
"icons/favicon.ico"
|
||||
]
|
||||
],
|
||||
"resources": {
|
||||
"../../config/composite.ksp-app-wallet-desk.json": "config/composite.ksp-app-wallet-desk.json",
|
||||
"../../config/std.logging.json": "config/std.logging.json",
|
||||
"../../config/std.transport.json": "config/std.transport.json",
|
||||
"../../config/std.wallet.json": "config/std.wallet.json",
|
||||
"../../config/schemas/composite.schema.json": "config/schemas/composite.schema.json",
|
||||
"../../config/schemas/std.logging.schema.json": "config/schemas/std.logging.schema.json",
|
||||
"../../config/schemas/std.transport.schema.json": "config/schemas/std.transport.schema.json",
|
||||
"../../config/schemas/std.wallet.schema.json": "config/schemas/std.wallet.schema.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/desktop_contract.rs
|
||||
// version: 19
|
||||
// version: 20
|
||||
|
||||
//! Desktop build, shell and Config-status contract audits for Wallet Desk.
|
||||
|
||||
@@ -417,3 +417,31 @@ fn pre_017_wallet_desk_open_paths_remain_non_migrating() {
|
||||
assert!(state.contains("ksp_wallet_lib::open_wallet_owner_file"));
|
||||
assert!(inventory.contains("ksp_wallet_lib::inspect_locked_wallet_file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_018_packaged_runtime_bundles_config_resources_and_keeps_wallet_desk_version_current() {
|
||||
let root = app_root();
|
||||
let tauri = read_json(root.join("tauri.conf.json").as_path());
|
||||
assert_eq!(tauri.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.6-pre.18"));
|
||||
let package = read_json(root.join("package.json").as_path());
|
||||
assert_eq!(package.pointer("/version").and_then(serde_json::Value::as_str), std::option::Option::Some("0.2.6-pre.18"));
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "packaged Wallet Desk Config resources map must exist");
|
||||
if let std::option::Option::Some(resources) = resources {
|
||||
assert_eq!(resources.len(), 8);
|
||||
assert_eq!(
|
||||
resources.get("../../config/composite.ksp-app-wallet-desk.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/composite.ksp-app-wallet-desk.json"),
|
||||
);
|
||||
assert_eq!(
|
||||
resources.get("../../config/schemas/composite.schema.json").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some("config/schemas/composite.schema.json"),
|
||||
);
|
||||
}
|
||||
let main = read_text(root.join("frontend/main.html").as_path());
|
||||
assert!(main.contains("0.2.6-pre.18"));
|
||||
let tauri_source = read_text(root.join("src/tauri.rs").as_path());
|
||||
assert!(tauri_source.contains("ksp_config_lib::prepare_packaged_runtime"));
|
||||
assert!(tauri_source.contains("tauri::utils::platform::resource_dir"));
|
||||
assert!(tauri_source.contains("std::env::set_current_dir(layout.runtime_root())"));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/tests/release_compliance.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Release-wide deterministic compliance canaries for Wallet Desk.
|
||||
|
||||
@@ -181,3 +181,36 @@ fn wallet_desk_dependency_firewall_and_tauri_capabilities_are_minimal() {
|
||||
assert!(!capability_source.contains("dialog:"));
|
||||
assert!(!capability_source.contains("fs:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_resources_include_only_registered_config_sources_and_schemas() {
|
||||
let root = app_root();
|
||||
let tauri_source = read_text(root.join("tauri.conf.json").as_path());
|
||||
let tauri = serde_json::from_str::<serde_json::Value>(tauri_source.as_str());
|
||||
assert!(tauri.is_ok(), "Wallet Desk Tauri config should parse");
|
||||
let tauri = match tauri {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return,
|
||||
};
|
||||
let resources = tauri.pointer("/bundle/resources").and_then(serde_json::Value::as_object);
|
||||
assert!(resources.is_some(), "Wallet Desk package must declare Config resources");
|
||||
let resources = match resources {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return,
|
||||
};
|
||||
assert_eq!(resources.len(), 8);
|
||||
for (source, destination) in resources {
|
||||
let destination = destination.as_str();
|
||||
assert!(destination.is_some(), "resource destination must be textual");
|
||||
let destination = match destination {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => continue,
|
||||
};
|
||||
assert!(source.starts_with("../../config/"));
|
||||
assert!(destination.starts_with("config/"));
|
||||
for forbidden in [".env", "wallets", "logs", "secret", "private"] {
|
||||
assert!(!source.contains(forbidden), "mutable/secret runtime resource must not be bundled: {source}");
|
||||
assert!(!destination.contains(forbidden), "mutable/secret runtime destination must not be bundled: {destination}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
167
crates/ksp-config-lib/src/packaging.rs
Normal file
167
crates/ksp-config-lib/src/packaging.rs
Normal 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;
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
90
crates/ksp-config-lib/unit_tests/packaging.rs
Normal file
90
crates/ksp-config-lib/unit_tests/packaging.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user