fix(lite-backend): durable wallet saves, non-destructive backup recovery
do_save flushed the temp wallet and renamed it over the wallet with no
fsync of the temp file, the .bak or the directory, so after a power cut
the rename could be on disk while the data was not, leaving a torn
wallet. The readers then panicked on malformed bytes instead of
returning an error (the gzip Decoder and WalletTx unwraps in
LightWallet::read, the version asserts in data.rs and walletzkey.rs,
from_utf8/diversifier unwraps, and vec![0; len] on a corrupt length
prefix, which can abort the process outright), so read_from_disk's .bak
fallback was never reached. And the fallback itself was destructive: it
copied the .bak over the wallet before reading it, on ANY error, so the
damaged file was lost, a transient I/O error replaced a good newer
wallet with an older .bak, and a wallet written by a newer app ("Don't
know how to read wallet version") was overwritten too, dropping imported
keys that are not in the seed.
- do_save: sync_all the temp file (removed on error), copy the .bak,
tighten and sync it through a write handle (FlushFileBuffers needs
write access on Windows), rename, sync the copy-fallback destination,
then fsync the directory on unix. No MoveFileEx write-through on
Windows.
- read_from_disk: each parse runs in read_wallet_file under
catch_unwind (panic = "unwind" is kept for release builds). The .bak
is used only when the wallet fails with InvalidData, UnexpectedEof or
InvalidInput, and it is read first, so a bad backup leaves both files
as they were. A damaged wallet whose header says encrypted is not
replaced by a plaintext .bak (only an older build leaves one): the
open fails with an explanation and nothing changes. Otherwise the
damaged wallet is moved (or, failing that, copied) to
<wallet>.dat.corrupt-<unix secs>, with a -N suffix while that name is
taken (a dangling symlink counts as taken), and is never deleted. The
recovered wallet is tightened as before.
- read_from_disk_reporting returns the same client plus "recovered from
backup; damaged file kept as <name>" after a recovery, for the FFI;
read_from_disk wraps it. Both stages are also written to stderr,
since the logger is not initialised yet when the wallet is opened.
- Readers return Err: a newer wallet version is ErrorKind::Unsupported
(was InvalidData), and so is a newer WalletTx or key version;
Decoder::new and WalletTx::read propagate with ?; a bad Utxo version,
a negative or non-UTF-8 or non-R Utxo address, non-UTF-8 metadata
addresses, an invalid diversifier and a viewing key with no default
address are InvalidData. Every length-prefixed string and address is
read through read_bytes_bounded (take(len).read_to_end, no cap, std's
own short-read error), so a corrupt length is never allocated up
front.
Flagged behaviour changes:
- fsync order: tmp, then .bak, then rename, then the dir (unix); saves
at exit, encrypt and decrypt still run on the UI thread and get
slower on slow disks;
- the readers return Err instead of panicking;
- the .bak fallback runs only for corrupt files, and the damaged file
is kept as .dat.corrupt-<ts> (every copy is kept; one from a
plaintext era keeps its seed, inside the 0700 dir);
- a wallet with a newer format errors (Unsupported) and is never
replaced;
- a damaged encrypted wallet whose backup predates encryption is not
opened.
Wallet format v9 is unchanged; valid files read and write byte for byte
as before. No ABI or symbol change.
Tests: a wallet whose parse panicked recovers from the .bak, the
garbage is kept as .dat.corrupt-<ts> and the report names it, and the
next open reports nothing; a newer-version wallet is Unsupported and
both files are unchanged (this fails when only the version error kind is
reverted); an unreadable (0000) wallet is a PermissionDenied error that
changes nothing either; a corrupt wallet with
a corrupt or no .bak changes nothing; a damaged encrypted wallet with a
pre-encryption plaintext .bak is refused with both files unchanged and
nothing moved aside; a recovery never overwrites an earlier
.dat.corrupt-<ts> or follows a dangling symlink; every truncation of a
wallet and of a WalletTx is an Err, crafted Utxo, metadata, note and key
records are the expected Err kinds, and a u64::MAX length returns at
once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+288
-19
@@ -587,6 +587,12 @@ impl LightClient {
|
||||
}
|
||||
|
||||
pub fn read_from_disk(config: &LightClientConfig) -> io::Result<Self> {
|
||||
Self::read_from_disk_reporting(config).map(|(lc, _)| lc)
|
||||
}
|
||||
|
||||
/// read_from_disk, plus what the open did to the files (s2-D14): Some("recovered from backup; damaged file kept as
|
||||
/// <name>") when a corrupt wallet was replaced by its backup, for the FFI to pass on.
|
||||
pub fn read_from_disk_reporting(config: &LightClientConfig) -> io::Result<(Self, Option<String>)> {
|
||||
if !config.wallet_exists() {
|
||||
// Try to recover from backup
|
||||
let bak_path = config.get_wallet_path().with_extension("dat.bak");
|
||||
@@ -601,25 +607,43 @@ impl LightClient {
|
||||
}
|
||||
}
|
||||
|
||||
// Try to open the wallet file; if it fails (corrupted/truncated), try the backup
|
||||
let wallet = match File::open(config.get_wallet_path())
|
||||
.and_then(|f| {
|
||||
let mut file_buffer = BufReader::new(f);
|
||||
LightWallet::read(&mut file_buffer, config)
|
||||
}) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
warn!("Failed to read wallet file: {}, trying backup", e);
|
||||
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 {
|
||||
// C244: fall back to the backup ONLY for a corrupt or torn file: a parser error, or a parser panic, which
|
||||
// read_wallet_file turns into InvalidData. Never for a transient I/O error (a sharing violation, EACCES) or a
|
||||
// wallet written by a newer app (Unsupported, s2-D4): both would replace good data with an older .bak.
|
||||
let wallet_path = config.get_wallet_path();
|
||||
let bak_path = wallet_path.with_extension("dat.bak");
|
||||
let (wallet, recovered) = match Self::read_wallet_file(&wallet_path, config) {
|
||||
Ok(w) => (w, None),
|
||||
Err(e) if matches!(e.kind(), ErrorKind::InvalidData | ErrorKind::UnexpectedEof | ErrorKind::InvalidInput) => {
|
||||
// The logger is not set up yet (the FFI initialises it after the open), so this goes to stderr.
|
||||
let _ = writeln!(io::stderr(), "Wallet file unreadable ({}); trying the backup", e);
|
||||
if !bak_path.exists() {
|
||||
return Err(e);
|
||||
}
|
||||
// Read the backup FIRST: if it is bad too, both files are left exactly as they were.
|
||||
let w = Self::read_wallet_file(&bak_path, config)?;
|
||||
// s2-D12: never silently drop wallet encryption. Only a .bak last saved by Lite <= 1.1.1 can predate
|
||||
// the encryption of its wallet (since s1-D6 the first encrypted save backs up the encrypted file).
|
||||
if !w.is_encrypted() && wallet_header_encrypted(&wallet_path) == Some(true) {
|
||||
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||
"The wallet file is damaged and its backup was made before the wallet was encrypted. \
|
||||
Nothing was changed. Restore from your seed phrase, or move the damaged wallet file aside \
|
||||
to open the older, unencrypted backup."));
|
||||
}
|
||||
// s2-D13: keep the damaged file, and never overwrite an earlier one: move it aside, or copy it aside
|
||||
// when it cannot be moved, before the backup replaces it.
|
||||
let corrupt = Self::unused_corrupt_path(&wallet_path);
|
||||
if let Err(re) = std::fs::rename(&wallet_path, &corrupt) {
|
||||
std::fs::copy(&wallet_path, &corrupt).map_err(|ce| io::Error::new(ce.kind(),
|
||||
format!("Couldn't keep the damaged wallet file: {} / {}", re, ce)))?;
|
||||
}
|
||||
std::fs::copy(&bak_path, &wallet_path)?;
|
||||
tighten(&wallet_path, false);
|
||||
let name = corrupt.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
let _ = writeln!(io::stderr(), "Wallet restored from backup; damaged file kept as {}", corrupt.display());
|
||||
(w, Some(format!("recovered from backup; damaged file kept as {}", name)))
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// C254: a wallet or .bak written before owner-only saves (0644 under the default umask) is healed on every
|
||||
@@ -643,7 +667,32 @@ impl LightClient {
|
||||
info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block());
|
||||
info!("Created LightClient to {}", &config.server);
|
||||
|
||||
Ok(lc)
|
||||
Ok((lc, recovered))
|
||||
}
|
||||
|
||||
/// C244: one parse of a wallet file. A parser panic on a corrupt file becomes InvalidData (lib/Cargo.toml keeps
|
||||
/// panic = "unwind" for release builds, so the catch is effective).
|
||||
fn read_wallet_file(path: &Path, config: &LightClientConfig) -> io::Result<LightWallet> {
|
||||
let f = File::open(path)?;
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| LightWallet::read(BufReader::new(f), config))) {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err(io::Error::new(ErrorKind::InvalidData, "wallet file is corrupt (parser panic)")),
|
||||
}
|
||||
}
|
||||
|
||||
/// s2-D13: <wallet>.dat.corrupt-<unix secs>, with a -1, -2, ... suffix while that name is taken. Any entry counts
|
||||
/// as taken, a dangling symlink too (symlink_metadata), so an earlier damaged file is never overwritten and a
|
||||
/// symlink is never followed.
|
||||
fn unused_corrupt_path(wallet_path: &Path) -> PathBuf {
|
||||
let ts = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||
let taken = |p: &Path| std::fs::symlink_metadata(p).is_ok();
|
||||
let mut p = wallet_path.with_extension(format!("dat.corrupt-{}", ts));
|
||||
let mut i: u64 = 1;
|
||||
while taken(&p) {
|
||||
p = wallet_path.with_extension(format!("dat.corrupt-{}-{}", ts, i));
|
||||
i += 1;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
pub fn init_logging(&self) -> io::Result<()> {
|
||||
@@ -869,6 +918,14 @@ impl LightClient {
|
||||
|
||||
// Only proceed with rename if the write succeeded
|
||||
if r.is_ok() {
|
||||
// C244: make the new bytes durable BEFORE they can replace the old file (FlushFileBuffers on Windows,
|
||||
// F_FULLFSYNC on macOS). Order: the tmp file, the .bak, the rename, then the directory (unix), so every
|
||||
// crash point leaves a complete wallet or a complete old wallet.
|
||||
file_buffer.get_ref().sync_all().map_err(|e| {
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
format!("Failed to sync wallet file: {}", e)
|
||||
})?;
|
||||
|
||||
// Create backup of existing wallet (if it exists)
|
||||
if wallet_path.exists() {
|
||||
// s1-D6: at the plaintext -> encrypted transition, back up the NEW (encrypted) file, so no
|
||||
@@ -876,8 +933,12 @@ impl LightClient {
|
||||
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),
|
||||
// fs::copy gives the .bak its source's mode: strip the group/other bits (C254). Then make it
|
||||
// durable (C244) through a write handle: FlushFileBuffers fails on a read-only one on Windows.
|
||||
Ok(_) => {
|
||||
tighten(&bak_path, false);
|
||||
if let Ok(f) = std::fs::OpenOptions::new().write(true).open(&bak_path) { let _ = f.sync_all(); }
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to create wallet backup: {}", e);
|
||||
// Non-fatal: continue with the save
|
||||
@@ -893,8 +954,17 @@ impl LightClient {
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
return Err(format!("Failed to save wallet: {} / {}", e, e2));
|
||||
}
|
||||
if let Ok(f) = std::fs::OpenOptions::new().write(true).open(&wallet_path) { let _ = f.sync_all(); }
|
||||
let _ = std::fs::remove_file(&tmp_path);
|
||||
}
|
||||
// C244: the rename is durable once its directory is. Unix only: a directory cannot be fsynced on
|
||||
// Windows, and the rename is not write-through there (s2-D3).
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if let Some(dir) = wallet_path.parent() {
|
||||
if let Ok(d) = File::open(dir) { let _ = d.sync_all(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2609,4 +2679,203 @@ pub fn test_bad_import() {
|
||||
}
|
||||
}
|
||||
|
||||
// C244 (s2 tests 1-3, the X5 test for s2-D12 and the s2-D13 no-overwrite test): read_from_disk falls back to the
|
||||
// .bak only for a corrupt wallet, keeps the damaged file, and changes nothing in every other case.
|
||||
fn corrupt_files(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
|
||||
let mut v: Vec<std::path::PathBuf> = std::fs::read_dir(dir).unwrap().map(|e| e.unwrap().path())
|
||||
.filter(|p| p.file_name().unwrap().to_string_lossy().contains(".dat.corrupt-")).collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
// A saved wallet and its .bak (the second save makes the .bak).
|
||||
fn saved_pair(tmp: &TempDir) -> (LightClientConfig, std::path::PathBuf, std::path::PathBuf) {
|
||||
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();
|
||||
lc.do_save().unwrap();
|
||||
assert!(bak.exists());
|
||||
(config, wallet, bak)
|
||||
}
|
||||
|
||||
// Version 5 selects the gzip path, and 0xAB is no gzip header: Decoder::new(..).unwrap() (LW:323) panicked on it.
|
||||
fn gzip_garbage() -> Vec<u8> {
|
||||
let mut b = 5u64.to_le_bytes().to_vec();
|
||||
b.extend_from_slice(&[0xAB; 32]);
|
||||
b
|
||||
}
|
||||
|
||||
fn read_err(config: &LightClientConfig) -> std::io::Error {
|
||||
match LightClient::read_from_disk(config) {
|
||||
Ok(_) => panic!("the wallet opened"),
|
||||
Err(e) => e,
|
||||
}
|
||||
}
|
||||
|
||||
// s2 C244 test 1: a parse that panicked recovers from the .bak, and the damaged file is kept as .dat.corrupt-<ts>.
|
||||
#[test]
|
||||
pub fn test_read_from_disk_recovers_when_parse_would_panic() {
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let (config, wallet, bak) = saved_pair(&tmp);
|
||||
let bak_bytes = std::fs::read(&bak).unwrap();
|
||||
std::fs::write(&wallet, gzip_garbage()).unwrap();
|
||||
|
||||
let (lc, note) = LightClient::read_from_disk_reporting(&config).unwrap();
|
||||
assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string());
|
||||
let kept = corrupt_files(tmp.path());
|
||||
assert_eq!(kept.len(), 1, "the damaged file is kept");
|
||||
assert_eq!(std::fs::read(&kept[0]).unwrap(), gzip_garbage());
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), bak_bytes, "the wallet is the backup now");
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), bak_bytes, "the backup is untouched");
|
||||
let name = kept[0].file_name().unwrap().to_string_lossy().into_owned();
|
||||
assert!(name.starts_with("silentdragonxlite-wallet.dat.corrupt-"), "{}", name);
|
||||
assert_eq!(note, Some(format!("recovered from backup; damaged file kept as {}", name)));
|
||||
drop(lc);
|
||||
|
||||
// The next open is a plain one: nothing to report, nothing moved.
|
||||
let (lc, note) = LightClient::read_from_disk_reporting(&config).unwrap();
|
||||
assert_eq!(note, None);
|
||||
assert_eq!(corrupt_files(tmp.path()), kept);
|
||||
assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string());
|
||||
}
|
||||
|
||||
// s2 C244 test 2 (s2-D4): a wallet written by a newer app is an Unsupported error that changes nothing; the older
|
||||
// .bak never replaces it. A transient I/O error (EACCES here) does not fall back either.
|
||||
#[test]
|
||||
pub fn test_read_from_disk_keeps_newer_version_wallet() {
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let (config, wallet, bak) = saved_pair(&tmp);
|
||||
let bak_bytes = std::fs::read(&bak).unwrap();
|
||||
let mut newer = u64::MAX.to_le_bytes().to_vec();
|
||||
newer.extend_from_slice(b"a wallet from a newer app");
|
||||
std::fs::write(&wallet, &newer).unwrap();
|
||||
|
||||
let e = read_err(&config);
|
||||
assert_eq!(e.kind(), std::io::ErrorKind::Unsupported, "{}", e);
|
||||
assert!(e.to_string().contains("Don't know how to read wallet version"), "{}", e);
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), newer, "the newer wallet was replaced");
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), bak_bytes, "the backup was changed");
|
||||
assert!(corrupt_files(tmp.path()).is_empty());
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::fs::write(&wallet, gzip_garbage()).unwrap();
|
||||
chmod(&wallet, 0o000);
|
||||
if std::fs::File::open(&wallet).is_err() { // not when the tests run as root
|
||||
let e = read_err(&config);
|
||||
chmod(&wallet, 0o600);
|
||||
assert_eq!(e.kind(), std::io::ErrorKind::PermissionDenied, "{}", e);
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), gzip_garbage(), "an unreadable wallet was replaced");
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), bak_bytes);
|
||||
assert!(corrupt_files(tmp.path()).is_empty());
|
||||
}
|
||||
chmod(&wallet, 0o600);
|
||||
}
|
||||
}
|
||||
|
||||
// s2 C244 test 3: a corrupt wallet with a corrupt (or no) .bak is an error that leaves the files as they were.
|
||||
#[test]
|
||||
pub fn test_read_from_disk_both_bad_leaves_files() {
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let (config, wallet, bak) = saved_pair(&tmp);
|
||||
let mut main_bad = std::fs::read(&wallet).unwrap();
|
||||
main_bad.truncate(main_bad.len() / 2);
|
||||
std::fs::write(&wallet, &main_bad).unwrap();
|
||||
std::fs::write(&bak, gzip_garbage()).unwrap();
|
||||
|
||||
let _ = read_err(&config);
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), main_bad, "the wallet was changed");
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), gzip_garbage(), "the backup was changed");
|
||||
assert!(corrupt_files(tmp.path()).is_empty());
|
||||
|
||||
std::fs::remove_file(&bak).unwrap();
|
||||
let e = read_err(&config);
|
||||
assert!(matches!(e.kind(), std::io::ErrorKind::InvalidData | std::io::ErrorKind::UnexpectedEof
|
||||
| std::io::ErrorKind::InvalidInput), "{:?}", e);
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), main_bad);
|
||||
assert!(!bak.exists());
|
||||
assert!(corrupt_files(tmp.path()).is_empty());
|
||||
}
|
||||
|
||||
// X5 (s2-D12): a damaged encrypted wallet whose .bak predates the encryption (a .bak last saved by Lite <= 1.1.1;
|
||||
// s1-D6 no longer writes one, so it is built by hand) is not opened: Err, both files unchanged, nothing kept aside.
|
||||
#[test]
|
||||
pub fn test_read_from_disk_refuses_plaintext_bak_for_encrypted_main() {
|
||||
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 lc = LightClient::new_from_phrase(TEST_SEED.to_string(), &config, 0, 0, false).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
let plaintext = std::fs::read(&wallet).unwrap();
|
||||
assert_eq!(plaintext[8], 0, "precondition: a plaintext wallet");
|
||||
lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap();
|
||||
lc.do_save().unwrap();
|
||||
drop(lc);
|
||||
|
||||
std::fs::write(&bak, &plaintext).unwrap();
|
||||
let enc = std::fs::read(&wallet).unwrap();
|
||||
assert_eq!(enc[8], 1, "precondition: an encrypted wallet");
|
||||
let damaged = enc[..enc.len() / 2].to_vec(); // the 9-byte header is intact
|
||||
std::fs::write(&wallet, &damaged).unwrap();
|
||||
assert_eq!(wallet_header_encrypted(&wallet), Some(true));
|
||||
let from_bak = LightWallet::read(std::io::BufReader::new(std::fs::File::open(&bak).unwrap()), &config).unwrap();
|
||||
assert!(!from_bak.is_encrypted(), "precondition: the .bak opens, unencrypted");
|
||||
assert_eq!(from_bak.get_seed_phrase(), TEST_SEED.to_string());
|
||||
|
||||
let e = read_err(&config);
|
||||
assert_eq!(e.kind(), std::io::ErrorKind::InvalidData, "{}", e);
|
||||
assert!(e.to_string().contains("its backup was made before the wallet was encrypted"), "{}", e);
|
||||
assert_eq!(std::fs::read(&wallet).unwrap(), damaged, "the damaged encrypted wallet was changed");
|
||||
assert_eq!(std::fs::read(&bak).unwrap(), plaintext, "the backup was changed");
|
||||
assert!(corrupt_files(tmp.path()).is_empty(), "a damaged file was moved aside");
|
||||
}
|
||||
|
||||
// s2-D13: a recovery never overwrites an earlier .dat.corrupt-<ts>; it takes the next free -<n> suffix. Any entry
|
||||
// counts as taken, a dangling symlink too (unix), whose target is never created.
|
||||
#[test]
|
||||
pub fn test_recovery_never_overwrites_previous_corrupt_file() {
|
||||
let tmp = TempDir::new("lctest").unwrap();
|
||||
let (config, wallet, _bak) = saved_pair(&tmp);
|
||||
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
|
||||
let mut sentinels = vec![];
|
||||
for t in now..now + 10 { // the recovery may run a few seconds later
|
||||
let p = wallet.with_extension(format!("dat.corrupt-{}", t));
|
||||
let s = format!("an earlier damaged wallet {}", t).into_bytes();
|
||||
std::fs::write(&p, &s).unwrap();
|
||||
sentinels.push((p, s));
|
||||
}
|
||||
#[cfg(unix)]
|
||||
let (want_suffix, target) = {
|
||||
let target = tmp.path().join("symlink-target");
|
||||
for t in now..now + 10 {
|
||||
std::os::unix::fs::symlink(&target, wallet.with_extension(format!("dat.corrupt-{}-1", t))).unwrap();
|
||||
}
|
||||
("-2", Some(target))
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let (want_suffix, target): (&str, Option<std::path::PathBuf>) = ("-1", None);
|
||||
|
||||
std::fs::write(&wallet, gzip_garbage()).unwrap();
|
||||
let (_lc, note) = LightClient::read_from_disk_reporting(&config).unwrap();
|
||||
for (p, s) in &sentinels {
|
||||
assert_eq!(&std::fs::read(p).unwrap(), s, "{:?} was overwritten", p);
|
||||
}
|
||||
let new: Vec<std::path::PathBuf> = corrupt_files(tmp.path()).into_iter()
|
||||
.filter(|p| std::fs::symlink_metadata(p).unwrap().file_type().is_file() && !sentinels.iter().any(|(q, _)| q == p))
|
||||
.collect();
|
||||
assert_eq!(new.len(), 1, "{:?}", new);
|
||||
let name = new[0].file_name().unwrap().to_string_lossy().into_owned();
|
||||
assert!(name.ends_with(want_suffix), "{}", name);
|
||||
assert_eq!(std::fs::read(&new[0]).unwrap(), gzip_garbage());
|
||||
assert_eq!(note, Some(format!("recovered from backup; damaged file kept as {}", name)));
|
||||
if let Some(t) = target {
|
||||
assert!(!t.exists(), "a dangling symlink was followed");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user