feat(wallets): recursive subdir scan + wallet.dat encryption/seed badges
Search user-added wallet folders RECURSIVELY (bounded: depth/hit/visit caps, skip_permission_denied, no symlink-follow, node-junk + node-subtree pruning); the datadir stays top-level. r.dir is each file's real parent so Open/import resolve subdir wallets. Auto-suffix the import destination on a name collision. Add util/wallet_file_probe.h: read encryption/seed/shielded flags straight off a wallet.dat WITHOUT loading it — validate the Berkeley DB btree magic, then a bounded streaming byte-scan for the length-prefixed record markers the daemon writes (mkey -> encrypted; hdseed/hdchain -> seed/HD; zkey/sapzkey -> shielded). Reads no key material. The Wallets dialog shows Encrypted / Seed / Legacy badges, plus an Unknown badge when a large file couldn't be fully scanned (so absence of a lock never falsely reads as unencrypted). Unit-tested + validated read-only against real wallets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -23,6 +25,7 @@
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/platform.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../../util/wallet_file_probe.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "../notifications.h"
|
||||
#include "../layout.h"
|
||||
@@ -134,7 +137,11 @@ public:
|
||||
|
||||
for (std::size_t i = 0; i < s_rows.size(); ++i) {
|
||||
const WalletRow& r = s_rows[i];
|
||||
const data::WalletIndexEntry* meta = app->walletIndex().find(r.fileName);
|
||||
// The wallet index is keyed by bare filename and is only ever written for the active
|
||||
// DATADIR wallet — so an external row (esp. a same-named one surfaced by the recursive
|
||||
// subdir scan) must NOT borrow a datadir wallet's cached balance/addresses. Only look up
|
||||
// metadata for datadir rows; external rows show size + "never opened" (accurate).
|
||||
const data::WalletIndexEntry* meta = r.inDatadir ? app->walletIndex().find(r.fileName) : nullptr;
|
||||
const bool isCurrent = r.inDatadir && r.fileName == active;
|
||||
|
||||
ImVec2 rMin = ImGui::GetCursorScreenPos();
|
||||
@@ -164,9 +171,23 @@ public:
|
||||
const float btnX = rMax.x - padX - btnW;
|
||||
const float textR = btnX - Layout::spacingMd();
|
||||
|
||||
// Name (+ info icon for external files)
|
||||
// Name (+ info icon for external files, + status badges from the no-daemon file probe)
|
||||
const float topY = rMin.y + cardPadY;
|
||||
float extra = (!r.inDatadir) ? (metaFont->LegacySize + Layout::spacingSm()) : 0.0f;
|
||||
// Right-aligned status badges: a lock (if encrypted) and a seed/legacy marker. Reserve their
|
||||
// width up front so the filename truncates before them instead of overlapping.
|
||||
const float badgeSz = metaFont->LegacySize;
|
||||
// Only positive detections are definitive; a MISSING marker is trustworthy solely when the
|
||||
// whole file was scanned (r.probeComplete). So: lock iff encrypted; seed iff hdSeed; "legacy"
|
||||
// only when complete AND no seed; and when the scan was truncated without confirming
|
||||
// encryption, an explicit "unknown" badge — so absence-of-lock never falsely reads as
|
||||
// "unencrypted" on a huge wallet whose mkey record sat past the probe cap.
|
||||
const bool bLock = r.probed && r.encrypted;
|
||||
const bool bSeed = r.probed && r.hdSeed;
|
||||
const bool bLegacy = r.probed && r.probeComplete && !r.hdSeed;
|
||||
const bool bUnknown = r.probed && !r.probeComplete && !r.encrypted;
|
||||
const int nBadges = (bLock ? 1 : 0) + (bSeed ? 1 : 0) + (bLegacy ? 1 : 0) + (bUnknown ? 1 : 0);
|
||||
const float badgeReserve = nBadges > 0 ? nBadges * (badgeSz + Layout::spacingSm()) + Layout::spacingSm() : 0.0f;
|
||||
float extra = ((!r.inDatadir) ? (metaFont->LegacySize + Layout::spacingSm()) : 0.0f) + badgeReserve;
|
||||
std::string nm = fit(r.fileName, nameFont, std::max(24.0f * dp, textR - x - extra));
|
||||
dl->AddText(nameFont, nameFont->LegacySize, ImVec2(x, topY), OnSurface(), nm.c_str());
|
||||
if (!r.inDatadir) {
|
||||
@@ -175,6 +196,20 @@ public:
|
||||
if (ImGui::IsMouseHoveringRect(ip, ImVec2(ip.x + metaFont->LegacySize, ip.y + metaFont->LegacySize)))
|
||||
Tooltip("%s\n%s", TR("wallets_external_tt"), r.dir.c_str());
|
||||
}
|
||||
if (nBadges > 0) {
|
||||
float bx = textR;
|
||||
auto badge = [&](const char* glyph, ImU32 col, const char* tip) {
|
||||
bx -= badgeSz;
|
||||
ImVec2 bp(bx, topY + (nameFont->LegacySize - badgeSz));
|
||||
dl->AddText(icoFont, badgeSz, bp, col, glyph);
|
||||
if (ImGui::IsMouseHoveringRect(bp, ImVec2(bp.x + badgeSz, bp.y + badgeSz))) Tooltip("%s", tip);
|
||||
bx -= Layout::spacingSm();
|
||||
};
|
||||
if (bLock) badge(ICON_MD_LOCK, WithAlpha(Warning(), 235), TR("wallets_badge_encrypted"));
|
||||
if (bSeed) badge(ICON_MD_ECO, WithAlpha(Success(), 220), TR("wallets_badge_seed"));
|
||||
if (bLegacy) badge(ICON_MD_HISTORY, WithAlpha(OnSurfaceMedium(), 210), TR("wallets_badge_legacy"));
|
||||
if (bUnknown) badge(ICON_MD_HELP_OUTLINE, WithAlpha(OnSurfaceMedium(), 170), TR("wallets_badge_unknown"));
|
||||
}
|
||||
|
||||
// Metadata line (size · N addresses · balance DRGX · last opened)
|
||||
std::string ms = util::Platform::formatFileSize((uint64_t)r.sizeBytes);
|
||||
@@ -288,6 +323,11 @@ private:
|
||||
std::string dir;
|
||||
bool inDatadir = false;
|
||||
long long sizeBytes = 0;
|
||||
// Light facts read off the wallet.dat file WITHOUT loading it (see util/wallet_file_probe.h).
|
||||
bool probed = false; // the file validated as a BDB wallet and was scanned
|
||||
bool probeComplete = false; // scan covered the whole file → a MISSING marker is trustworthy
|
||||
bool encrypted = false; // passphrase-encrypted (has an mkey record)
|
||||
bool hdSeed = false; // HD/seed wallet (else legacy) — ties into migrate-to-seed
|
||||
};
|
||||
|
||||
// Force a "wallet-...dat" filename (plain, no path) so a new/imported wallet shows in the
|
||||
@@ -312,12 +352,21 @@ private:
|
||||
// from there) under a wallet-*.dat name, then switch to it. Never overwrites an existing file.
|
||||
static void importAndOpen(App* app, const WalletRow& r) {
|
||||
namespace fs = std::filesystem;
|
||||
std::string dest = normalizeWalletName(r.fileName);
|
||||
if (dest.empty()) { Notifications::instance().warning(TR("wallets_name_invalid")); return; }
|
||||
const std::string base = normalizeWalletName(r.fileName);
|
||||
if (base.empty()) { Notifications::instance().warning(TR("wallets_name_invalid")); return; }
|
||||
std::error_code ec;
|
||||
const std::string datadir = util::Platform::getDragonXDataDir();
|
||||
fs::create_directories(datadir, ec);
|
||||
const std::string destPath = datadir + "/" + dest;
|
||||
// The recursive subdir scan can surface several distinct wallets sharing one basename (e.g. a
|
||||
// "wallet.dat" in two backup folders). Auto-suffix the datadir destination to the next free name
|
||||
// instead of hard-failing, so the second one is importable too. Never overwrites an existing file.
|
||||
const std::string stem = base.substr(0, base.size() - 4); // strip ".dat"
|
||||
std::string dest = base;
|
||||
std::string destPath = datadir + "/" + dest;
|
||||
for (int n = 2; n < 1000 && fs::exists(destPath, ec); ++n) {
|
||||
dest = stem + "-" + std::to_string(n) + ".dat";
|
||||
destPath = datadir + "/" + dest;
|
||||
}
|
||||
if (fs::exists(destPath, ec)) { Notifications::instance().warning(TR("wallets_exists")); return; }
|
||||
fs::copy_file(r.dir + "/" + r.fileName, destPath, ec);
|
||||
if (ec) { Notifications::instance().error(TR("wallets_import_failed")); return; }
|
||||
@@ -327,32 +376,98 @@ private:
|
||||
}
|
||||
|
||||
// Scan the datadir (wallet-prefixed *.dat only, to skip peers.dat / asmap.dat / fee_estimates.dat)
|
||||
// and each user-added folder (any *.dat). Exception-safe: iterates with error_codes.
|
||||
// and each user-added folder (any *.dat). User-added folders are searched RECURSIVELY into
|
||||
// subdirectories; the datadir stays top-level only (never descend into blocks/ chainstate/ …).
|
||||
// Exception-safe: iterates with error_codes. Recursion is bounded (depth / hit / visit caps) and
|
||||
// never follows directory symlinks (std default), so it can't loop or stall the UI on a huge tree.
|
||||
static void scan() {
|
||||
s_rows.clear();
|
||||
namespace fs = std::filesystem;
|
||||
auto addFrom = [](const std::string& dir, bool inDatadir) {
|
||||
// Guards for recursive external-folder scans (a user could point us at a large tree / home dir).
|
||||
constexpr int kMaxDepth = 8; // don't descend deeper than this below the chosen folder
|
||||
constexpr size_t kMaxHits = 200; // cap total wallet files listed
|
||||
constexpr int kMaxVisited = 40000; // cap total entries walked, so scan() can't stall the UI
|
||||
|
||||
// Shared budget for the no-daemon wallet.dat probes (encryption / seed badges) so a pile of large
|
||||
// wallets can't turn scan() into a multi-hundred-MB read on the UI thread; each file is capped too.
|
||||
// Cover realistic wallets fully (the active wallet can be well over 100 MB of tx history, and a
|
||||
// BDB btree sorts long-keyed records — mkey/hdchain — LATE, so a small cap risks a false-negative
|
||||
// badge). Per-file 256 MB + a shared budget keep even that a bounded, one-shot read on dialog open.
|
||||
std::size_t probeBudget = 768u * 1024u * 1024u;
|
||||
constexpr std::size_t kProbePerFile = 256u * 1024u * 1024u;
|
||||
|
||||
// Node/daemon .dat files that are NOT wallets. The datadir scan already restricts to a "wallet"
|
||||
// prefix, but recursive external folders accept any *.dat — and a user can easily point the picker
|
||||
// at a folder that IS or CONTAINS a datadir, so filter these out everywhere or the list fills with
|
||||
// blk*/rev* block files, peers.dat, etc. presented as importable "wallets".
|
||||
auto isNodeArtifact = [](const std::string& n) {
|
||||
static const char* kExact[] = { "peers.dat", "banlist.dat", "fee_estimates.dat",
|
||||
"mempool.dat", "asmap.dat", "zindex.dat" };
|
||||
for (const char* e : kExact) if (n == e) return true;
|
||||
return n.rfind("blk", 0) == 0 || n.rfind("rev", 0) == 0; // blk00000.dat / rev00000.dat
|
||||
};
|
||||
auto consider = [&](const fs::path& p, bool inDatadir) {
|
||||
// String-only checks FIRST (no syscalls) — the vast majority of entries in a large tree are
|
||||
// neither .dat nor wallets, so reject them before paying for a stat().
|
||||
const std::string name = p.filename().string();
|
||||
if (name.size() <= 4 || name.substr(name.size() - 4) != ".dat") return;
|
||||
if (inDatadir) { if (name.rfind("wallet", 0) != 0) return; } // datadir: wallet-prefixed only
|
||||
else { if (isNodeArtifact(name)) return; } // external: reject node junk
|
||||
std::error_code fec;
|
||||
if (!fs::is_regular_file(p, fec)) return;
|
||||
WalletRow r;
|
||||
r.fileName = name;
|
||||
r.dir = p.parent_path().string(); // the file's ACTUAL parent (may be a subdir) — Open reads r.dir + "/" + r.fileName
|
||||
r.inDatadir = inDatadir;
|
||||
r.sizeBytes = static_cast<long long>(fs::file_size(p, fec));
|
||||
// Read encryption / seed flags straight off the file (no daemon), within the byte budget.
|
||||
if (probeBudget > 0) {
|
||||
const auto pr = util::probeWalletFile(p.string(), std::min(probeBudget, kProbePerFile));
|
||||
r.probed = pr.isBerkeleyDB;
|
||||
r.probeComplete = pr.scanComplete;
|
||||
r.encrypted = pr.encrypted;
|
||||
r.hdSeed = pr.hdSeed;
|
||||
// Charge the budget by bytes ACTUALLY read (early-exit reads far less than a big file),
|
||||
// so a few large wallets don't starve later rows of their probe.
|
||||
probeBudget -= std::min(probeBudget, pr.bytesRead);
|
||||
}
|
||||
s_rows.push_back(std::move(r));
|
||||
};
|
||||
// Directory names we never descend into during a recursive external scan — node data subtrees hold
|
||||
// no user wallets and would otherwise burn the visit budget (and flood the list) with block files.
|
||||
auto isNodeSubtree = [](const std::string& n) {
|
||||
return n == "blocks" || n == "chainstate" || n == "database" ||
|
||||
n == "indexes" || n == "index";
|
||||
};
|
||||
auto addFrom = [&](const std::string& dir, bool inDatadir, bool recursive) {
|
||||
std::error_code ec;
|
||||
if (dir.empty() || !fs::is_directory(dir, ec)) return;
|
||||
fs::directory_iterator it(dir, ec), end;
|
||||
if (!recursive) {
|
||||
fs::directory_iterator it(dir, ec), end;
|
||||
for (; !ec && it != end; it.increment(ec)) consider(it->path(), inDatadir);
|
||||
return;
|
||||
}
|
||||
// skip_permission_denied keeps a locked subdir from aborting the whole walk; the iterator
|
||||
// does NOT follow directory symlinks by default, so symlink cycles can't cause infinite loops.
|
||||
fs::recursive_directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec), end;
|
||||
int visited = 0;
|
||||
for (; !ec && it != end; it.increment(ec)) {
|
||||
std::error_code fec;
|
||||
if (!it->is_regular_file(fec)) continue;
|
||||
std::string name = it->path().filename().string();
|
||||
if (name.size() <= 4 || name.substr(name.size() - 4) != ".dat") continue;
|
||||
if (inDatadir && name.rfind("wallet", 0) != 0) continue;
|
||||
WalletRow r;
|
||||
r.fileName = name;
|
||||
r.dir = dir;
|
||||
r.inDatadir = inDatadir;
|
||||
r.sizeBytes = static_cast<long long>(fs::file_size(it->path(), fec));
|
||||
s_rows.push_back(std::move(r));
|
||||
if (++visited > kMaxVisited) break;
|
||||
std::error_code dec;
|
||||
if (it->is_directory(dec)) {
|
||||
// Prune: don't descend past the depth cap or into node data subtrees.
|
||||
if (it.depth() >= kMaxDepth || isNodeSubtree(it->path().filename().string()))
|
||||
it.disable_recursion_pending();
|
||||
continue; // directories aren't wallet files
|
||||
}
|
||||
consider(it->path(), inDatadir);
|
||||
if (s_rows.size() >= kMaxHits) break;
|
||||
}
|
||||
};
|
||||
addFrom(util::Platform::getDragonXDataDir(), /*inDatadir=*/true);
|
||||
addFrom(util::Platform::getDragonXDataDir(), /*inDatadir=*/true, /*recursive=*/false);
|
||||
if (s_app)
|
||||
for (const auto& f : s_app->walletIndex().extraFolders())
|
||||
addFrom(f, /*inDatadir=*/false);
|
||||
addFrom(f, /*inDatadir=*/false, /*recursive=*/true);
|
||||
}
|
||||
|
||||
static inline bool s_open = false;
|
||||
|
||||
@@ -262,6 +262,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["wallets_col_opened"] = "Last opened";
|
||||
strings_["wallets_current"] = "current";
|
||||
strings_["wallets_active"] = "Active";
|
||||
strings_["wallets_badge_encrypted"] = "Encrypted (passphrase-protected)";
|
||||
strings_["wallets_badge_seed"] = "Seed phrase wallet (HD)";
|
||||
strings_["wallets_badge_legacy"] = "Legacy wallet (no seed phrase)";
|
||||
strings_["wallets_badge_unknown"] = "Wallet type not fully determined (large file — open to confirm)";
|
||||
strings_["wallets_open"] = "Open";
|
||||
strings_["wallets_never"] = "Never";
|
||||
strings_["wallets_import_hint"] = "Import to open";
|
||||
|
||||
122
src/util/wallet_file_probe.h
Normal file
122
src/util/wallet_file_probe.h
Normal file
@@ -0,0 +1,122 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// wallet_file_probe.h — read light metadata off a wallet.dat WITHOUT loading it into the daemon.
|
||||
//
|
||||
// DragonX wallet.dat is a Berkeley DB (btree) key/value store. Only the private-key material is
|
||||
// encrypted (ckey/czkey/csapzkey/chdseed); the record *keys* and non-secret metadata live in
|
||||
// plaintext, so a handful of boolean facts can be recovered cheaply by (a) validating the BDB btree
|
||||
// magic and (b) scanning the raw bytes for the length-prefixed record names the daemon writes
|
||||
// (a std::string is serialized as CompactSize(len)+bytes, so e.g. an "mkey" record's key begins with
|
||||
// the 5 bytes 0x04 'm' 'k' 'e' 'y'). This is a heuristic PRESENCE test — reliable for booleans, not
|
||||
// for exact counts — with no libdb dependency and no daemon. It never reads or exposes key material.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace util {
|
||||
|
||||
struct WalletFileProbe {
|
||||
bool isBerkeleyDB = false; ///< file has a valid BDB btree metapage magic (looks like a real wallet.dat)
|
||||
bool encrypted = false; ///< has an "mkey" master-key record → passphrase-encrypted
|
||||
bool hdSeed = false; ///< has hdseed/chdseed/hdchain → HD/seed wallet (else legacy)
|
||||
bool hasShielded = false; ///< has zkey/czkey/sapzkey/csapzkey → holds shielded addresses
|
||||
// A found marker is always definitive; a MISSING marker is only trustworthy when the scan actually
|
||||
// covered the whole file. `scanComplete` is true when we reached EOF, or early-exited having found
|
||||
// everything — false only when the byte cap cut the scan short. Callers should treat negative flags
|
||||
// (e.g. "legacy" = !hdSeed, or "not encrypted" = !encrypted) as reliable only when scanComplete is true.
|
||||
bool scanComplete = false;
|
||||
std::size_t bytesRead = 0; ///< bytes actually consumed (early-exit reads far less than the file) — for budgeting
|
||||
};
|
||||
|
||||
// Probe a wallet.dat by header-validating it as a BDB btree then byte-scanning (bounded, streaming,
|
||||
// early-exit) for record markers. `maxBytes` caps how far we read so a pathologically large file can't
|
||||
// stall the caller. Returns an all-false probe (isBerkeleyDB=false) for anything that isn't a readable
|
||||
// BDB btree file. Safe to call on a wallet currently open by the daemon (read-only, pattern scan only).
|
||||
inline WalletFileProbe probeWalletFile(const std::string& path,
|
||||
std::size_t maxBytes = 96u * 1024u * 1024u) {
|
||||
WalletFileProbe out;
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) return out;
|
||||
|
||||
// --- 1) Validate the Berkeley DB btree metapage magic (offset 12, either endianness). ---
|
||||
unsigned char hdr[16];
|
||||
f.read(reinterpret_cast<char*>(hdr), sizeof(hdr));
|
||||
if (f.gcount() < static_cast<std::streamsize>(sizeof(hdr))) return out;
|
||||
auto rd32 = [](const unsigned char* p, bool le) -> uint32_t {
|
||||
return le ? (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24)
|
||||
: (uint32_t)p[3] | ((uint32_t)p[2] << 8) | ((uint32_t)p[1] << 16) | ((uint32_t)p[0] << 24);
|
||||
};
|
||||
constexpr uint32_t kBtreeMagic = 0x00053162u; // DB_BTREEMAGIC
|
||||
if (rd32(hdr + 12, true) != kBtreeMagic && rd32(hdr + 12, false) != kBtreeMagic) return out;
|
||||
out.isBerkeleyDB = true;
|
||||
|
||||
// --- 2) Streaming byte-scan for length-prefixed record markers. ---
|
||||
// group: 0 = encrypted, 1 = hd/seed, 2 = shielded.
|
||||
const std::vector<std::pair<std::string, int>> patterns = {
|
||||
{std::string("\x04", 1) + "mkey", 0}, // master key → wallet is encrypted
|
||||
{std::string("\x06", 1) + "hdseed", 1}, // plaintext HD seed
|
||||
{std::string("\x07", 1) + "chdseed", 1}, // encrypted HD seed
|
||||
{std::string("\x07", 1) + "hdchain", 1}, // HD chain counter (present in both HD variants)
|
||||
{std::string("\x04", 1) + "zkey", 2}, // sprout shielded key
|
||||
{std::string("\x05", 1) + "czkey", 2}, // encrypted sprout key
|
||||
{std::string("\x07", 1) + "sapzkey", 2}, // sapling shielded key
|
||||
{std::string("\x08", 1) + "csapzkey", 2}, // encrypted sapling key
|
||||
};
|
||||
std::size_t maxPat = 0;
|
||||
for (const auto& p : patterns) maxPat = std::max(maxPat, p.first.size());
|
||||
const std::size_t overlap = maxPat > 0 ? maxPat - 1 : 0; // bytes carried between chunks for split matches
|
||||
|
||||
f.clear();
|
||||
f.seekg(0, std::ios::beg);
|
||||
constexpr std::size_t kChunk = 1u << 20; // 1 MiB
|
||||
std::vector<char> buf(kChunk);
|
||||
std::string carry;
|
||||
std::size_t readTotal = 0;
|
||||
bool enc = false, hd = false, sh = false;
|
||||
bool eof = false, allFound = false;
|
||||
while (readTotal < maxBytes) {
|
||||
const std::size_t want = std::min(kChunk, maxBytes - readTotal);
|
||||
f.read(buf.data(), static_cast<std::streamsize>(want));
|
||||
const std::streamsize got = f.gcount();
|
||||
if (got <= 0) { eof = true; break; }
|
||||
readTotal += static_cast<std::size_t>(got);
|
||||
|
||||
std::string window;
|
||||
window.reserve(carry.size() + static_cast<std::size_t>(got));
|
||||
window.assign(carry);
|
||||
window.append(buf.data(), static_cast<std::size_t>(got));
|
||||
|
||||
for (const auto& p : patterns) {
|
||||
bool& flag = (p.second == 0) ? enc : (p.second == 1) ? hd : sh;
|
||||
if (!flag && window.find(p.first) != std::string::npos) flag = true;
|
||||
}
|
||||
if (enc && hd && sh) { allFound = true; break; } // nothing more to learn
|
||||
|
||||
if (window.size() > overlap) carry.assign(window.data() + window.size() - overlap, overlap);
|
||||
else carry.assign(window);
|
||||
if (static_cast<std::size_t>(got) < want) { eof = true; break; } // reached EOF
|
||||
}
|
||||
out.encrypted = enc;
|
||||
out.hdSeed = hd;
|
||||
out.hasShielded = sh;
|
||||
out.bytesRead = readTotal;
|
||||
// false only if the byte cap cut a larger file short. If we stopped because readTotal hit maxBytes on
|
||||
// an exact chunk boundary, ifstream never set eofbit — peek() tells us whether the file actually ended
|
||||
// there (EOF → complete) or there's more beyond the cap (a real byte → truncated).
|
||||
out.scanComplete = allFound || eof;
|
||||
if (!out.scanComplete && f && f.peek() == std::ifstream::traits_type::eof()) out.scanComplete = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
} // namespace dragonx
|
||||
@@ -36,6 +36,7 @@
|
||||
#include "util/pool_registry.h"
|
||||
#include "util/daemon_updater.h"
|
||||
#include "util/lite_server_probe.h"
|
||||
#include "util/wallet_file_probe.h"
|
||||
#include "wallet/lite_connection_service.h"
|
||||
#include "wallet/lite_diagnostics.h"
|
||||
#include "wallet/lite_owned_string.h"
|
||||
@@ -737,6 +738,60 @@ void testPaymentUri()
|
||||
EXPECT_EQ(invalid.error, std::string("Invalid negative amount"));
|
||||
}
|
||||
|
||||
void testWalletFileProbe()
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
using dragonx::util::probeWalletFile;
|
||||
const fs::path dir = fs::path("/tmp/dragonx") / "probe";
|
||||
std::error_code ec; fs::create_directories(dir, ec);
|
||||
auto writef = [](const fs::path& p, const std::string& d) {
|
||||
std::ofstream o(p, std::ios::binary); o.write(d.data(), (std::streamsize)d.size());
|
||||
};
|
||||
// A valid Berkeley DB btree metapage has the magic 0x00053162 at byte offset 12 (little-endian here).
|
||||
auto bdb = []() { std::string h(16, '\0'); h[12] = 0x62; h[13] = 0x31; h[14] = 0x05; h[15] = 0x00; return h; };
|
||||
// Wallet records are keyed by a CompactSize-length-prefixed name, e.g. "mkey" -> {0x04,'m','k','e','y'}.
|
||||
auto rec = [](const std::string& name) { return std::string(1, (char)name.size()) + name; };
|
||||
|
||||
// 1) Non-BDB file → not recognized, no flags.
|
||||
writef(dir / "junk.dat", std::string(4096, 'x'));
|
||||
{ auto p = probeWalletFile((dir / "junk.dat").string());
|
||||
EXPECT_FALSE(p.isBerkeleyDB); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed); }
|
||||
|
||||
// 2) BDB with no markers → legacy plaintext; a complete scan makes the negatives trustworthy.
|
||||
writef(dir / "legacy.dat", bdb() + std::string(2000, '\0'));
|
||||
{ auto p = probeWalletFile((dir / "legacy.dat").string());
|
||||
EXPECT_TRUE(p.isBerkeleyDB); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed);
|
||||
EXPECT_FALSE(p.hasShielded); EXPECT_TRUE(p.scanComplete); }
|
||||
|
||||
// 3) Encrypted (mkey) + HD (hdchain) + shielded (csapzkey).
|
||||
writef(dir / "enc.dat", bdb() + std::string(500, 'a') + rec("mkey") + std::string(300, 'b') +
|
||||
rec("hdchain") + std::string(300, 'c') + rec("csapzkey") + std::string(500, 'd'));
|
||||
{ auto p = probeWalletFile((dir / "enc.dat").string());
|
||||
EXPECT_TRUE(p.encrypted); EXPECT_TRUE(p.hdSeed); EXPECT_TRUE(p.hasShielded); }
|
||||
|
||||
// 4) False-positive guard: the bare word "mkey" WITHOUT the 0x04 length prefix must not count.
|
||||
writef(dir / "bare.dat", bdb() + std::string(100, '\0') + "mkey" + std::string(100, '\0'));
|
||||
{ auto p = probeWalletFile((dir / "bare.dat").string()); EXPECT_FALSE(p.encrypted); }
|
||||
|
||||
// 5) Byte cap: a marker beyond maxBytes is missed AND the scan is flagged incomplete (so a caller
|
||||
// won't wrongly assert "legacy"); the same file scanned fully finds it and reports complete.
|
||||
{ std::string d = bdb(); d.resize(2u * 1024 * 1024, 'q'); d += rec("mkey"); writef(dir / "deep.dat", d);
|
||||
auto capped = probeWalletFile((dir / "deep.dat").string(), 1024u * 1024u);
|
||||
EXPECT_TRUE(capped.isBerkeleyDB); EXPECT_FALSE(capped.encrypted); EXPECT_FALSE(capped.scanComplete);
|
||||
EXPECT_TRUE(capped.bytesRead <= 1024u * 1024u); // budget accounting reflects a truncated read
|
||||
auto full = probeWalletFile((dir / "deep.dat").string());
|
||||
EXPECT_TRUE(full.encrypted); EXPECT_TRUE(full.scanComplete); }
|
||||
|
||||
// 6) Exact-boundary case: a file whose length is an exact 1 MiB multiple (and == maxBytes) must still
|
||||
// report scanComplete=true (the peek() guard), so a fully-scanned wallet isn't mislabeled truncated.
|
||||
{ std::string d = bdb(); d.resize(1024u * 1024u, 'e'); // exactly one 1 MiB chunk, no markers
|
||||
writef(dir / "exact.dat", d);
|
||||
auto p = probeWalletFile((dir / "exact.dat").string(), 1024u * 1024u);
|
||||
EXPECT_TRUE(p.isBerkeleyDB); EXPECT_TRUE(p.scanComplete); EXPECT_FALSE(p.encrypted); EXPECT_FALSE(p.hdSeed); }
|
||||
|
||||
fs::remove_all(dir, ec);
|
||||
}
|
||||
|
||||
void testAmountFormatting()
|
||||
{
|
||||
EXPECT_EQ(dragonx::util::formatAmountFixed(1.0), std::string("1.00000000"));
|
||||
@@ -6263,6 +6318,7 @@ int main()
|
||||
testHushChatOutgoing();
|
||||
testHushChatTransport();
|
||||
testHushChatShuffledReceive();
|
||||
testWalletFileProbe();
|
||||
testAddressChecksumValidation();
|
||||
testLiteServerProbeLive();
|
||||
testXmrigLiveInstall();
|
||||
|
||||
Reference in New Issue
Block a user