diff --git a/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs b/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs index 6dcdd06..ddbcfab 100644 --- a/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs +++ b/third_party/silentdragonxlite/silentdragonxlite-cli/lib/src/lightclient.rs @@ -47,6 +47,83 @@ pub const DEFAULT_SERVER: &str = "https://lite.dragonx.is"; pub const WALLET_NAME: &str = "silentdragonxlite-wallet.dat"; pub const LOGFILE_NAME: &str = "silentdragonxlite-wallet.debug.log"; +// C254: the data dir, the wallet files and the log are kept private to the owner on unix. On Windows these helpers +// are the plain std calls (the per-user AppData ACL confines the files). Each one is an item-level +// #[cfg(unix)] / #[cfg(not(unix))] pair, so the mingw build has nothing unused. + +/// A chmod that fails (drvfs /mnt/c, FAT, NFS root-squash, a file of another uid) is warned about once per process: +/// get_zcash_data_path() runs on every save. An AtomicBool, not a Once (which poisons if its closure panics). +#[cfg(unix)] +static PERM_WARNED: AtomicBool = AtomicBool::new(false); + +#[cfg(unix)] +fn perm_warn(p: &Path, e: &io::Error) { + if !PERM_WARNED.swap(true, Ordering::Relaxed) { + warn!("Couldn't restrict {:?}: {}", p, e); + } +} + +/// Create `d` and any missing parents; on unix every created component is 0700. +#[cfg(unix)] +fn create_private_dir_all(d: &Path) -> io::Result<()> { + use std::os::unix::fs::DirBuilderExt; + std::fs::DirBuilder::new().recursive(true).mode(0o700).create(d) +} +#[cfg(not(unix))] +fn create_private_dir_all(d: &Path) -> io::Result<()> { + std::fs::create_dir_all(d) +} + +/// Strip the group/other bits from an existing dir (`want_dir`) or regular file. The owner bits are kept (none is +/// ever added), a symlink is never followed, and a missing path or a path of the other kind is left alone. +#[cfg(unix)] +fn tighten(p: &Path, want_dir: bool) { + use std::os::unix::fs::PermissionsExt; + if let Ok(md) = std::fs::symlink_metadata(p) { + let m = md.permissions().mode(); + let kind_ok = if want_dir { md.is_dir() } else { md.is_file() }; + if kind_ok && m & 0o077 != 0 { + if let Err(e) = std::fs::set_permissions(p, std::fs::Permissions::from_mode(m & 0o7700)) { + perm_warn(p, &e); + } + } + } +} +#[cfg(not(unix))] +fn tighten(_p: &Path, _want_dir: bool) {} + +/// A fresh inode, owner-only from creation (s1-D9). A stale file at `p` (a crash-leftover .dat.tmp, or a symlink) is +/// unlinked, never reused or followed, so an fd opened on it earlier never sees the new content. A concurrent creator +/// in between makes this fail, which is the safe outcome. +#[cfg(unix)] +fn create_private_file_new(p: &Path) -> io::Result { + use std::os::unix::fs::OpenOptionsExt; + match std::fs::remove_file(p) { + Ok(()) => {}, + Err(ref e) if e.kind() == ErrorKind::NotFound => {}, + Err(e) => return Err(e), + } + std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(p) +} +#[cfg(not(unix))] +fn create_private_file_new(p: &Path) -> io::Result { + File::create(p) +} + +/// X3: the encryption flag in a wallet file's header (u64 LE version, then the flag byte that LightWallet::read reads +/// for every version >= 4). None when the header cannot tell: a short read or an I/O error, v5 (gzip: its byte 8 is +/// not the flag), v<=3 (no flag) or v>9 (a format this build does not know). +fn wallet_header_encrypted(p: &Path) -> Option { + let mut h = [0u8; 9]; + File::open(p).and_then(|mut f| f.read_exact(&mut h)).ok()?; + let mut v = [0u8; 8]; + v.copy_from_slice(&h[..8]); + match u64::from_le_bytes(v) { + 4 | 6 | 7 | 8 | 9 => Some(h[8] != 0), + _ => None, + } +} + #[derive(Clone, Debug)] pub struct WalletStatus { pub is_syncing: bool, @@ -117,11 +194,35 @@ impl LightClientConfig { } + /// The log roller's file pattern, anchored in the data dir (s1-D7): rolled logs stay inside the owner-only data + /// dir instead of the process's working directory (which may be read-only, on Windows too). FixedWindowRoller + /// replaces every "{}" in the pattern, so a data path that contains "{}" itself (or is not UTF-8) keeps the old + /// relative pattern. + fn log_roll_pattern(&self) -> String { + const PATTERN: &str = "zecwallet-light-wallet-log{}"; + let data_dir = self.get_zcash_data_path(); + match (data_dir.to_str(), data_dir.join(PATTERN).to_str()) { + (Some(d), Some(p)) if !d.contains("{}") => p.to_string(), + _ => PATTERN.to_string(), + } + } + /// Build the Logging config pub fn get_log_config(&self) -> io::Result { + // C254: on unix the log is created owner-only (0600) before log4rs opens it for appending (log4rs 0.8.3 has + // no mode option), and an existing one loses its group/other bits. That mode lasts only until the first 5 MB + // roll, after which log4rs re-creates the file with the umask mode: the 0700 data dir is the lasting control + // (s1-D11). + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + let log_path = self.get_log_path(); + let _ = std::fs::OpenOptions::new().append(true).create(true).mode(0o600).open(&log_path); + tighten(&log_path, false); + } let window_size = 3; // log0, log1, log2 let fixed_window_roller = - FixedWindowRoller::builder().build("zecwallet-light-wallet-log{}",window_size).unwrap(); + FixedWindowRoller::builder().build(&self.log_roll_pattern(),window_size).unwrap(); let size_limit = 5 * 1024 * 1024; // 5MB as max log file size to roll let size_trigger = SizeTrigger::new(size_limit); let compound_policy = CompoundPolicy::new(Box::new(size_trigger),Box::new(fixed_window_roller)); @@ -168,9 +269,27 @@ impl LightClientConfig { }; } - // Create directory if it doesn't exist - match std::fs::create_dir_all(zcash_data_location.clone()) { - Ok(_) => zcash_data_location.into_boxed_path(), + // Create directory if it doesn't exist. C254: a caller-supplied data_dir (tests, embedders) is created as + // before and never chmodded. The default one is created owner-only (0700 on unix), and an existing one (for + // test/regtest also its parent) loses its group/other bits; the owner bits are kept and a symlink is never + // followed. The params dir (get_zcash_params_path) is left alone (s1-D5). + let created = if self.data_dir.is_some() { + std::fs::create_dir_all(zcash_data_location.clone()) + } else { + create_private_dir_all(&zcash_data_location) + }; + match created { + Ok(_) => { + if self.data_dir.is_none() { + tighten(&zcash_data_location, true); + if self.chain_name != "main" { + if let Some(parent) = zcash_data_location.parent() { + tighten(parent, true); + } + } + } + zcash_data_location.into_boxed_path() + }, Err(e) => { eprintln!("Couldn't create zcash directory!\n{}", e); panic!("Couldn't create zcash directory!"); @@ -474,6 +593,7 @@ impl LightClient { if bak_path.exists() { warn!("Wallet file missing but backup found, attempting recovery from {:?}", bak_path); std::fs::copy(&bak_path, config.get_wallet_path())?; + tighten(&config.get_wallet_path(), false); info!("Wallet recovered from backup"); } else { return Err(Error::new(ErrorKind::AlreadyExists, @@ -493,6 +613,7 @@ impl LightClient { let bak_path = config.get_wallet_path().with_extension("dat.bak"); if bak_path.exists() { std::fs::copy(&bak_path, config.get_wallet_path())?; + tighten(&config.get_wallet_path(), false); let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?); LightWallet::read(&mut file_buffer, config)? } else { @@ -501,6 +622,11 @@ impl LightClient { } }; + // C254: a wallet or .bak written before owner-only saves (0644 under the default umask) is healed on every + // open, not only at the next save. + tighten(&config.get_wallet_path(), false); + tighten(&config.get_wallet_path().with_extension("dat.bak"), false); + let mut lc = LightClient { wallet : Arc::new(RwLock::new(wallet)), config : config.clone(), @@ -718,11 +844,12 @@ impl LightClient { let tmp_path = wallet_path.with_extension("dat.tmp"); let bak_path = wallet_path.with_extension("dat.bak"); - // Write to a temporary file first (atomic save) + // Write to a temporary file first (atomic save). C254/s1-D9: a fresh owner-only file (0600 on unix); a stale + // .dat.tmp is unlinked first, never reused. The rename carries the mode to the wallet. let wallet = self.wallet.write().unwrap(); let mut file_buffer = BufWriter::with_capacity( 1_000_000, // 1 MB write buffer - File::create(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?); + create_private_file_new(&tmp_path).map_err(|e| format!("Failed to create temp file: {}", e))?); r = match wallet.write(&mut file_buffer) { Ok(_) => Ok(()), @@ -744,9 +871,17 @@ impl LightClient { if r.is_ok() { // Create backup of existing wallet (if it exists) if wallet_path.exists() { - if let Err(e) = std::fs::copy(&wallet_path, &bak_path) { - warn!("Failed to create wallet backup: {}", e); - // Non-fatal: continue with the save + // s1-D6: at the plaintext -> encrypted transition, back up the NEW (encrypted) file, so no + // plaintext seed survives in .bak + let bak_src: &Path = if wallet.is_encrypted() && wallet_header_encrypted(&wallet_path) == Some(false) + { tmp_path.as_path() } else { &*wallet_path }; + match std::fs::copy(bak_src, &bak_path) { + // fs::copy gives the .bak its source's mode: strip the group/other bits (C254) + Ok(_) => tighten(&bak_path, false), + Err(e) => { + warn!("Failed to create wallet backup: {}", e); + // Non-fatal: continue with the save + } } } @@ -2147,4 +2282,331 @@ pub fn test_bad_import() { assert_eq!(one_panic, Err("t-address scan job panicked".to_string())); } + // C254 tests (s1 tests 9-14). Every mode is set explicitly before it is checked, never left to the umask. + #[cfg(unix)] + fn mode(p: &std::path::Path) -> u32 { + use std::os::unix::fs::PermissionsExt; + std::fs::symlink_metadata(p).unwrap().permissions().mode() & 0o7777 + } + + #[cfg(unix)] + fn chmod(p: &std::path::Path, m: u32) { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(p, std::fs::Permissions::from_mode(m)).unwrap(); + } + + // s1 test 9: the wallet and its .bak are owner-only (0600) after every save and after an open, whatever their + // earlier mode, and a wallet restored from a loose .bak is owner-only too. The caller's data dir is never chmodded. + #[cfg(unix)] + #[test] + pub fn test_wallet_files_private() { + let tmp = TempDir::new("lctest").unwrap(); + chmod(tmp.path(), 0o755); + let config = main_config(&tmp); + let wallet = config.get_wallet_path().into_path_buf(); + let bak = wallet.with_extension("dat.bak"); + let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap(); + lc.do_save().unwrap(); + assert!(bak.exists()); + for save in 1..=2 { + chmod(&wallet, 0o644); + chmod(&bak, 0o644); + lc.do_save().unwrap(); + assert_eq!(mode(&wallet), 0o600, "the wallet after save {}", save); + assert_eq!(mode(&bak), 0o600, "the .bak after save {}", save); + } + drop(lc); + + // An open heals both files before any save. + chmod(&wallet, 0o644); + chmod(&bak, 0o644); + let lc = LightClient::read_from_disk(&config).unwrap(); + assert_eq!(mode(&wallet), 0o600, "the wallet after an open"); + assert_eq!(mode(&bak), 0o600, "the .bak after an open"); + drop(lc); + + // A missing wallet restored from a loose .bak is owner-only. + std::fs::remove_file(&wallet).unwrap(); + chmod(&bak, 0o644); + let lc = LightClient::read_from_disk(&config).unwrap(); + assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string()); + assert_eq!(mode(&wallet), 0o600, "the wallet restored from the .bak"); + + assert!(!wallet.with_extension("dat.tmp").exists()); + assert_eq!(mode(tmp.path()), 0o755, "a caller-supplied data dir is never chmodded"); + } + + // s1 test 10 (D9): a crash-leftover 0644 .dat.tmp is unlinked, not reused. The save writes a fresh 0600 inode, so + // an fd opened on the stale tmp earlier (another account's) never sees the new wallet. + #[cfg(unix)] + #[test] + pub fn test_stale_tmp_replaced() { + use std::io::Read; + use std::os::unix::fs::MetadataExt; + let tmp = TempDir::new("lctest").unwrap(); + let config = main_config(&tmp); + let wallet = config.get_wallet_path().into_path_buf(); + let tmp_path = wallet.with_extension("dat.tmp"); + let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap(); + + std::fs::write(&tmp_path, b"stale").unwrap(); + chmod(&tmp_path, 0o644); + let mut held = std::fs::File::open(&tmp_path).unwrap(); + let stale_ino = held.metadata().unwrap().ino(); + lc.do_save().unwrap(); + + assert!(!tmp_path.exists(), "a .dat.tmp is left behind"); + assert_eq!(mode(&wallet), 0o600); + assert_ne!(std::fs::metadata(&wallet).unwrap().ino(), stale_ino, "the stale tmp's inode was reused"); + let mut seen = Vec::new(); + held.read_to_end(&mut seen).unwrap(); + assert_eq!(seen, b"stale".to_vec(), "the stale tmp's fd sees the new wallet"); + drop(lc); + let lc = LightClient::read_from_disk(&config).unwrap(); + assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string()); + } + + // s1 test 11: the helpers' semantics. + #[cfg(unix)] + #[test] + pub fn test_tighten_semantics() { + use std::os::unix::fs::MetadataExt; + use super::{tighten, create_private_dir_all, create_private_file_new}; + let tmp = TempDir::new("lctest").unwrap(); + let t = tmp.path(); + + // Only the group/other bits go; the owner bits stay as they were (none is added). + for &(from, to) in &[(0o755, 0o700), (0o777, 0o700), (0o750, 0o700), (0o700, 0o700), (0o555, 0o500), (0o500, 0o500)] { + let d = t.join(format!("dir{:o}", from)); + std::fs::create_dir(&d).unwrap(); + chmod(&d, from); + tighten(&d, false); + assert_eq!(mode(&d), from, "tighten(dir {:o}, want_dir=false) changed it", from); + tighten(&d, true); + assert_eq!(mode(&d), to, "dir {:o}", from); + } + for &(from, to) in &[(0o644, 0o600), (0o666, 0o600), (0o640, 0o600), (0o600, 0o600), (0o444, 0o400), (0o400, 0o400)] { + let f = t.join(format!("file{:o}", from)); + std::fs::write(&f, b"x").unwrap(); + chmod(&f, from); + tighten(&f, true); + assert_eq!(mode(&f), from, "tighten(file {:o}, want_dir=true) changed it", from); + tighten(&f, false); + assert_eq!(mode(&f), to, "file {:o}", from); + } + tighten(&t.join("missing"), true); + tighten(&t.join("missing"), false); + + // A symlink is never followed. + let target_dir = t.join("target-dir"); + std::fs::create_dir(&target_dir).unwrap(); + chmod(&target_dir, 0o755); + let target_file = t.join("target-file"); + std::fs::write(&target_file, b"x").unwrap(); + chmod(&target_file, 0o644); + std::os::unix::fs::symlink(&target_dir, t.join("link-dir")).unwrap(); + std::os::unix::fs::symlink(&target_file, t.join("link-file")).unwrap(); + tighten(&t.join("link-dir"), true); + tighten(&t.join("link-file"), false); + assert_eq!(mode(&target_dir), 0o755, "tighten followed a dir symlink"); + assert_eq!(mode(&target_file), 0o644, "tighten followed a file symlink"); + + // create_private_dir_all: every created component is 0700; an existing dir is accepted as it is. + create_private_dir_all(&t.join("a").join("b").join("c")).unwrap(); + for p in &[t.join("a"), t.join("a").join("b"), t.join("a").join("b").join("c")] { + assert_eq!(mode(p), 0o700, "{:?}", p); + } + create_private_dir_all(&target_dir).unwrap(); + assert_eq!(mode(&target_dir), 0o755); + + // create_private_file_new: a fresh 0600 inode; a stale file or a symlink at the path is unlinked, never + // reused or followed; a directory at the path is an error and stays. + let fresh = t.join("fresh.tmp"); + create_private_file_new(&fresh).unwrap(); + assert_eq!(mode(&fresh), 0o600); + let stale = t.join("stale.tmp"); + std::fs::write(&stale, b"stale").unwrap(); + chmod(&stale, 0o644); + let stale_ino = std::fs::metadata(&stale).unwrap().ino(); + let _held = std::fs::File::open(&stale).unwrap(); + create_private_file_new(&stale).unwrap(); + assert_eq!(mode(&stale), 0o600); + assert_eq!(std::fs::read(&stale).unwrap().len(), 0); + assert_ne!(std::fs::metadata(&stale).unwrap().ino(), stale_ino); + let via_link = t.join("via-link.tmp"); + std::os::unix::fs::symlink(&target_file, &via_link).unwrap(); + create_private_file_new(&via_link).unwrap(); + assert!(std::fs::symlink_metadata(&via_link).unwrap().is_file(), "the symlink was followed"); + assert_eq!(mode(&via_link), 0o600); + assert_eq!(std::fs::read(&target_file).unwrap(), b"x".to_vec()); + assert_eq!(mode(&target_file), 0o644); + assert!(create_private_file_new(&target_dir).is_err()); + assert!(target_dir.is_dir()); + + // A caller-supplied data_dir is never chmodded. + chmod(t, 0o755); + let config = main_config(&tmp); + assert_eq!(&*config.get_zcash_data_path(), t); + assert_eq!(mode(t), 0o755, "a caller-supplied data dir was chmodded"); + } + + // s1 test 12 (D6): the first save after encrypt() backs up the new, encrypted file, so no plaintext seed survives + // in .bak; that .bak is a full copy that unlocks to the seed. Later saves back up the encrypted wallet too. + #[test] + pub fn test_bak_not_plaintext_after_encrypt() { + use crate::lightwallet::LightWallet; + use super::wallet_header_encrypted; + let tmp = TempDir::new("lctest").unwrap(); + let config = main_config(&tmp); + let wallet = config.get_wallet_path().into_path_buf(); + let bak = wallet.with_extension("dat.bak"); + let header = |p: &std::path::Path| { + let b = std::fs::read(p).unwrap(); + (b[..8].to_vec(), b[8]) + }; + let v9 = 9u64.to_le_bytes().to_vec(); + let lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap(); + lc.do_save().unwrap(); + assert_eq!(header(&bak), (v9.clone(), 0), "precondition: a plaintext v9 .bak"); + assert_eq!(wallet_header_encrypted(&wallet), Some(false)); + + lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap(); + lc.do_save().unwrap(); + assert_eq!(header(&wallet), (v9.clone(), 1), "the wallet is saved encrypted, as v9"); + assert_eq!(header(&bak), (v9.clone(), 1), "the .bak still holds the plaintext wallet after the encrypt save"); + assert_eq!(wallet_header_encrypted(&bak), Some(true)); + assert_eq!(std::fs::read(&bak).unwrap(), std::fs::read(&wallet).unwrap()); + let mut from_bak = LightWallet::read(std::io::BufReader::new(std::fs::File::open(&bak).unwrap()), &config).unwrap(); + assert!(from_bak.is_encrypted()); + from_bak.unlock("password".to_string()).unwrap(); + assert_eq!(from_bak.get_seed_phrase(), TEST_SEED.to_string()); + + lc.do_save().unwrap(); + assert_eq!(header(&bak).1, 1, "a later save backs up the encrypted wallet"); + // Removing encryption is not the transition: its save backs up the encrypted wallet as before. + lc.wallet.write().unwrap().remove_encryption("password".to_string()).unwrap(); + lc.do_save().unwrap(); + assert_eq!(header(&wallet).1, 0, "the wallet is saved decrypted"); + assert_eq!(header(&bak).1, 1); + } + + // X3: wallet_header_encrypted reads the flag only where LightWallet::read has one at byte 8 (v4 and v6-v9). + #[test] + pub fn test_wallet_header_encrypted_probe() { + use super::wallet_header_encrypted; + let tmp = TempDir::new("lctest").unwrap(); + let p = tmp.path().join("w.dat"); + let probe = |v: u64, rest: &[u8]| { + let mut b = v.to_le_bytes().to_vec(); + b.extend_from_slice(rest); + std::fs::write(&p, &b).unwrap(); + wallet_header_encrypted(&p) + }; + for &v in &[4u64, 6, 7, 8, 9] { + assert_eq!(probe(v, &[0, 7, 7]), Some(false), "v{}", v); + assert_eq!(probe(v, &[1]), Some(true), "v{}", v); + assert_eq!(probe(v, &[2]), Some(true), "v{}: any non-zero flag, as LightWallet::read", v); + assert_eq!(probe(v, &[]), None, "v{} without a flag byte", v); + } + for &v in &[0u64, 1, 2, 3, 5, 10, u64::MAX] { + assert_eq!(probe(v, &[0]), None, "v{}", v); + assert_eq!(probe(v, &[1]), None, "v{}", v); + } + std::fs::write(&p, b"").unwrap(); + assert_eq!(wallet_header_encrypted(&p), None, "an empty file"); + assert_eq!(wallet_header_encrypted(&tmp.path().join("missing.dat")), None, "a missing file"); + assert_eq!(wallet_header_encrypted(tmp.path()), None, "a directory"); + } + + // s1 test 13 (D7, D11): get_log_config() creates the log owner-only and tightens an existing one; the roller's + // pattern lies in the data dir, so a roll renames the log inside it; a data path containing "{}" keeps the old + // relative pattern (FixedWindowRoller replaces every "{}"). + #[cfg(unix)] + #[test] + pub fn test_log_paths() { + use log4rs::append::rolling_file::policy::compound::roll::{Roll, fixed_window::FixedWindowRoller}; + let tmp = TempDir::new("lctest").unwrap(); + let config = main_config(&tmp); + let log = config.get_log_path().into_path_buf(); + assert!(!log.exists()); + config.get_log_config().unwrap(); + assert_eq!(mode(&log), 0o600, "a new log"); + chmod(&log, 0o644); + config.get_log_config().unwrap(); + assert_eq!(mode(&log), 0o600, "an existing loose log"); + + let pattern = config.log_roll_pattern(); + assert_eq!(pattern, tmp.path().join("zecwallet-light-wallet-log{}").to_str().unwrap()); + FixedWindowRoller::builder().build(&pattern, 3).unwrap().roll(&log).unwrap(); + assert!(!log.exists()); + assert_eq!(mode(&tmp.path().join("zecwallet-light-wallet-log0")), 0o600, "the rolled log is not in the data dir"); + + let odd = tmp.path().join("we{}ird"); + let odd_config = LightClientConfig::create_unconnected("main".to_string(), odd.to_str().map(|s| s.to_string())); + assert_eq!(odd_config.log_roll_pattern(), "zecwallet-light-wallet-log{}"); + odd_config.get_log_config().unwrap(); + assert_eq!(mode(&odd_config.get_log_path()), 0o600); + } + + // s1 test 14, opt-in: the default data dir (no data_dir) is created 0700, a pre-made 0755 one becomes 0700 (for + // test/regtest also its parent), and HOME, the params dir and a symlinked data dir are left alone. HOME points at + // a fresh temp dir for the test's own duration, so no real ~/.silentdragonxlite is ever seen; still, run it alone + // and only through rtest.sh (scratch HOME): + // DRAGONX_LITE_SCRATCH_HOME=1 rtest.sh --lib --offline test_default_data_dir_private -- --ignored + #[cfg(all(unix, not(target_os = "macos")))] + #[test] + #[ignore] + pub fn test_default_data_dir_private() { + if std::env::var("DRAGONX_LITE_SCRATCH_HOME").ok().as_deref() != Some("1") { + eprintln!("test_default_data_dir_private: needs DRAGONX_LITE_SCRATCH_HOME=1 (through rtest.sh); skipped"); + return; + } + let home = TempDir::new("lchome").unwrap(); + chmod(home.path(), 0o755); + let saved_home = std::env::var_os("HOME"); + std::env::set_var("HOME", home.path()); + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let h = home.path(); + let data = h.join(".silentdragonxlite"); + let main = LightClientConfig::create_unconnected("main".to_string(), None); + assert!(!data.exists()); + assert!(!main.wallet_exists()); + assert_eq!(&*main.get_zcash_data_path(), data.as_path()); + assert_eq!(mode(&data), 0o700, "a new default data dir"); + chmod(&data, 0o755); + assert!(!main.wallet_exists()); + assert_eq!(mode(&data), 0o700, "a pre-made 0755 data dir"); + + chmod(&data, 0o755); + let test = LightClientConfig::create_unconnected("test".to_string(), None); + assert!(!test.wallet_exists()); + assert_eq!(mode(&data.join("testnet3")), 0o700, "a new testnet3 dir"); + assert_eq!(mode(&data), 0o700, "the parent of testnet3"); + + let params = h.join(".zcash-params"); + std::fs::create_dir(¶ms).unwrap(); + chmod(¶ms, 0o755); + main.get_zcash_params_path().unwrap(); + assert_eq!(mode(¶ms), 0o755, "the params dir was chmodded"); + assert_eq!(mode(h), 0o755, "HOME was chmodded"); + + let h2 = h.join("h2"); + let real = h.join("real"); + std::fs::create_dir(&h2).unwrap(); + std::fs::create_dir(&real).unwrap(); + chmod(&real, 0o755); + std::os::unix::fs::symlink(&real, h2.join(".silentdragonxlite")).unwrap(); + std::env::set_var("HOME", &h2); + assert!(!main.wallet_exists()); + assert_eq!(mode(&real), 0o755, "a symlinked data dir was followed"); + })); + match saved_home { + Some(v) => std::env::set_var("HOME", v), + None => std::env::remove_var("HOME"), + } + if let Err(e) = r { + std::panic::resume_unwind(e); + } + } + }