87 lines
4.0 KiB
C++
87 lines
4.0 KiB
C++
#include "prune_reconcile.h"
|
||
|
||
#include <unordered_set>
|
||
|
||
// prune_reconcile implementation — the one set-algebra computation, kept trivially
|
||
// auditable: build the referenced and owned lookup sets, then walk `present` once,
|
||
// keeping a path iff it is owned AND not referenced. Walking `present` (not owned)
|
||
// gives the ∩-present clause for free and yields output in folder-enumeration order.
|
||
|
||
namespace reasampler {
|
||
|
||
std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
|
||
const std::vector<std::string>& referenced,
|
||
const std::vector<std::string>& owned) {
|
||
// Exact-string membership — the model's canonical relative-path comparison
|
||
// (Sample.relativePath / OwnedFileManifest::contains). std::string hashes/compares
|
||
// byte-for-byte, so no normalization creeps in.
|
||
const std::unordered_set<std::string> referencedSet(referenced.begin(),
|
||
referenced.end());
|
||
const std::unordered_set<std::string> ownedSet(owned.begin(), owned.end());
|
||
|
||
std::vector<std::string> orphans;
|
||
std::unordered_set<std::string> emitted; // de-dup repeated spellings in `present`
|
||
|
||
for (const std::string& path : present) {
|
||
// (owned ∩ present) − referenced: present is the walk; owned and !referenced
|
||
// are the two membership tests; emitted guards against a duplicate `present`.
|
||
if (ownedSet.count(path) == 0) continue; // not our leaving — skip
|
||
if (referencedSet.count(path) != 0) continue; // some bank references it
|
||
if (!emitted.insert(path).second) continue; // already emitted
|
||
orphans.push_back(path);
|
||
}
|
||
return orphans;
|
||
}
|
||
|
||
std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary,
|
||
const std::vector<std::string>& extra) {
|
||
std::vector<std::string> merged;
|
||
merged.reserve(primary.size() + extra.size());
|
||
std::unordered_set<std::string> seen;
|
||
for (const std::string& p : primary) {
|
||
if (seen.insert(p).second) merged.push_back(p);
|
||
}
|
||
for (const std::string& p : extra) {
|
||
if (seen.insert(p).second) merged.push_back(p);
|
||
}
|
||
return merged;
|
||
}
|
||
|
||
PruneReport buildPruneReport(
|
||
const std::vector<std::string>& orphans,
|
||
const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
|
||
std::size_t displayCap) {
|
||
PruneReport report;
|
||
report.count = orphans.size();
|
||
for (const std::string& o : orphans) {
|
||
// Sum EVERY orphan's bytes (the exact reclaimable total), not just the displayed
|
||
// ones. A path with no stat'd size contributes 0 — never dropped, never negative.
|
||
const auto it = sizeByPath.find(o);
|
||
report.totalBytes += (it != sizeByPath.end()) ? it->second : 0;
|
||
// Display list is capped (0 = uncapped). count stays exact above regardless.
|
||
if (displayCap == 0 || report.orphans.size() < displayCap)
|
||
report.orphans.push_back(o);
|
||
}
|
||
report.truncated = report.orphans.size() < report.count;
|
||
return report;
|
||
}
|
||
|
||
std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
|
||
const std::vector<std::string>& freshOrphans) {
|
||
// fresh is a pure-core output (referenced/hand-dropped already excluded), so keeping a
|
||
// confirmed path iff it is still a fresh orphan can never re-admit an unsafe file. Walk
|
||
// `confirmed` to preserve the confirm's listing order; emitted de-dups repeats.
|
||
const std::unordered_set<std::string> freshSet(freshOrphans.begin(), freshOrphans.end());
|
||
|
||
std::vector<std::string> plan;
|
||
std::unordered_set<std::string> emitted;
|
||
for (const std::string& path : confirmed) {
|
||
if (freshSet.count(path) == 0) continue; // stale: vanished / now-referenced -> skip
|
||
if (!emitted.insert(path).second) continue; // already emitted
|
||
plan.push_back(path);
|
||
}
|
||
return plan;
|
||
}
|
||
|
||
} // namespace reasampler
|