// file: ks-wallet/tests/legacy_characterization.rs // version: 2 //! External characterization tests for the legacy Solana JSON wallet format. use solana_signer::Signer; // rust-rules: trait-import use zeroize::Zeroize; // rust-rules: trait-import fn write_legacy_fixture(path: &std::path::Path, keypair: &solana_keypair::Keypair) { let mut keypair_bytes = keypair.to_bytes(); let encoded = serde_json::to_vec(keypair_bytes.as_slice()) .unwrap_or_else(|error| panic!("legacy fixture must serialize: {error}")); keypair_bytes.zeroize(); std::fs::write(path, encoded) .unwrap_or_else(|error| panic!("legacy fixture must be writable: {error}")); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) .unwrap_or_else(|error| panic!("legacy fixture permissions must be set: {error}")); } } #[test] fn legacy_path_is_alias_json() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let alias = ks_wallet::WalletAlias::parse("legacy-name") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); assert_eq!( store.wallet_path(&alias).file_name(), std::option::Option::Some(std::ffi::OsStr::new("legacy-name.json")) ); } #[tokio::test] async fn legacy_standard_json_fixture_preserves_public_key() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let alias = ks_wallet::WalletAlias::parse("legacy-fixture") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let expected = solana_keypair::Keypair::new(); write_legacy_fixture(&store.wallet_path(&alias), &expected); let loaded = store .load(alias) .await .unwrap_or_else(|error| panic!("legacy fixture must load: {error}")); assert_eq!(loaded.public_key(), expected.pubkey().to_string()); assert_eq!(loaded.persistence(), ks_wallet::WalletPersistence::Persistent); } #[tokio::test] async fn legacy_missing_file_preserves_current_error_contract() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let alias = ks_wallet::WalletAlias::parse("missing") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let error = store .load(alias) .await .err() .unwrap_or_else(|| panic!("missing legacy wallet must fail")); assert_eq!(error.code(), "wallet_file_metadata_failed"); } #[tokio::test] async fn legacy_errors_and_debug_do_not_disclose_local_path() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let canary_directory = directory.path().join("wallet-path-canary"); std::fs::create_dir_all(&canary_directory) .unwrap_or_else(|error| panic!("canary directory must be created: {error}")); let store = ks_wallet::TemporaryWalletStore::new(&canary_directory) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let debug = format!("{store:?}"); assert!(!debug.contains("wallet-path-canary")); assert!(!debug.contains(canary_directory.to_string_lossy().as_ref())); let alias = ks_wallet::WalletAlias::parse("missing-canary") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let error = store .load(alias) .await .err() .unwrap_or_else(|| panic!("missing legacy wallet must fail")); let message = error.to_string(); assert_eq!(error.code(), "wallet_file_metadata_failed"); assert!(!message.contains("wallet-path-canary")); assert!(!message.contains(canary_directory.to_string_lossy().as_ref())); } #[tokio::test] async fn legacy_corrupted_json_and_short_keypair_are_distinct() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let corrupted_alias = ks_wallet::WalletAlias::parse("corrupted-json") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let corrupted_path = store.wallet_path(&corrupted_alias); std::fs::write(&corrupted_path, b"not-json") .unwrap_or_else(|error| panic!("corrupted fixture must be writable: {error}")); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(&corrupted_path, std::fs::Permissions::from_mode(0o600)) .unwrap_or_else(|error| panic!("fixture permissions must be set: {error}")); } let corrupted_error = store .load(corrupted_alias) .await .err() .unwrap_or_else(|| panic!("corrupted JSON must fail")); assert_eq!(corrupted_error.code(), "wallet_keypair_json_invalid"); let short_alias = ks_wallet::WalletAlias::parse("short-keypair") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let short_path = store.wallet_path(&short_alias); std::fs::write(&short_path, b"[1,2,3]") .unwrap_or_else(|error| panic!("short fixture must be writable: {error}")); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(&short_path, std::fs::Permissions::from_mode(0o600)) .unwrap_or_else(|error| panic!("fixture permissions must be set: {error}")); } let short_error = store .load(short_alias) .await .err() .unwrap_or_else(|| panic!("short keypair must fail")); assert_eq!(short_error.code(), "wallet_keypair_length_invalid"); } #[tokio::test] async fn legacy_create_refuses_existing_wallet_and_preserves_first_pubkey() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let alias = ks_wallet::WalletAlias::parse("collision") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let first = store .create(alias.clone()) .await .unwrap_or_else(|error| panic!("first legacy wallet must be created: {error}")); let second_error = store .create(alias.clone()) .await .err() .unwrap_or_else(|| panic!("second legacy create must fail")); assert_eq!(second_error.code(), "wallet_file_already_exists"); let loaded = store .load(alias) .await .unwrap_or_else(|error| panic!("first legacy wallet must remain readable: {error}")); assert_eq!(loaded.public_key(), first.public_key()); } #[cfg(unix)] #[tokio::test] async fn legacy_file_with_open_permissions_is_rejected() { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let alias = ks_wallet::WalletAlias::parse("open-permissions") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let keypair = solana_keypair::Keypair::new(); let path = store.wallet_path(&alias); write_legacy_fixture(&path, &keypair); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)) .unwrap_or_else(|error| panic!("fixture permissions must be changed: {error}")); let error = store .load(alias) .await .err() .unwrap_or_else(|| panic!("open permissions must fail")); assert_eq!(error.code(), "wallet_file_permissions_too_open"); } #[cfg(unix)] #[tokio::test] async fn legacy_symlink_is_rejected() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); let store = ks_wallet::TemporaryWalletStore::new(directory.path()) .unwrap_or_else(|error| panic!("unexpected store error: {error}")); let target_alias = ks_wallet::WalletAlias::parse("target") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let link_alias = ks_wallet::WalletAlias::parse("linked") .unwrap_or_else(|error| panic!("unexpected alias error: {error}")); let keypair = solana_keypair::Keypair::new(); let target_path = store.wallet_path(&target_alias); write_legacy_fixture(&target_path, &keypair); std::os::unix::fs::symlink(&target_path, store.wallet_path(&link_alias)) .unwrap_or_else(|error| panic!("legacy symlink fixture must be created: {error}")); let error = store .load(link_alias) .await .err() .unwrap_or_else(|| panic!("legacy symlink must fail")); assert_eq!(error.code(), "wallet_file_type_invalid"); }