[Gelöst] Mehrfach-Dateien erkennen C++ und std::filesystem
-
@Schlangenmensch sagte in Mehrfach-Dateien erkennen C++ und std::filesystem:
Kryptografische Hashfunktionen: Es ist hinreichend schwer eine Kollision zu finden (Pre-image resistance, Second pre-image resistance und collsion resistance)
Ich verstehe schon was SeppJ meint.
So einfach darf man nicht Hash Funktion und kryptografische Hashfunktionen in einen Topf werfen. Folgende zusätzliche Eigenschaften von kryptografischen Hashfunktionen kenne ich noch:
- Kryptografische Hashfunktionen sind Einwegfunktionen. In die eine Richtung lassen sie sich einfach rechnen. Die Umkehrrechnung jedoch nicht, da diese eine Komplexität von aufweist. (siehe auch https://en.wikipedia.org/wiki/One-way_function)
- Das Ergebnis muss eine hohe Entropie aufweisen.
- Das Ergebnis darf keine Rückschlüsse auf den Eingabetext erlauben.
- Das Ergebnis darf keine Sprach-Korrelanzen aufweisen.
- Avalanche Effekt: Eine Änderung eines Bits in der Eingabe muss zu einer kompletten anderen Ausgabe führen. (https://en.wikipedia.org/wiki/Avalanche_effect)
- Das Verfahren muss resistent gegenüber aktuellen Angrifsstechniken sein (z.B. https://en.wikipedia.org/wiki/Length_extension_attack)
-
Also ich würde es vermutlich so machen:
- Pfade und Grössen sammeln
- Eindeutig einzigartige Files anhand der Grösse ermitteln und rauswerfen
- Von allem was übrig geblieben ist einen Hash über die ersten paar kB erstellen
- Eindeutig einzigartige Files anhand der Grösse + partiellem Hash ermitteln und rauswerfen
Danach könnte man dann entweder einen Hash über den gesamten Inhalt machen.
Oder man könnte einen weiteren partiellen Hash über einen grösseren Bereich machen und schauen was danach noch übrig geblieben ist. Dann einen nochmal grösseren Bereich usw. bis man alle verbliebenen Files vollständig gehasht hat.
Evtl. könnte es auch Sinn machen zusätzlich zu den ersten paar kB auch die letzten paar kB zu hashen. Gibt ja ein paar File-Formate die sich in bestimmten Fällen erst sehr weit hinten unterscheiden. Z.B. ZIP Archive die sich erst in Files unterscheiden die zuletzt hinzugefügt wurden. Die könnte man dann schneller rauswerfen.
Weiters würde es vermutlich auch Sinn machen die Inodes der Files zu ermitteln. Denn zwei Files die die selbe Inode haben (und auf dem selben Volume liegen) sind ja notwendigerweise identisch. Die sollte das Programm dann nicht vergleichen. Und ggf. auch gesondert ausweisen. Denn wenn es z.B. darum geht dass man Speicherplatz sparen will indem man Duplikate löscht, dann könnte es interessant sein dass diese "Duplikate" gar keinen Speicherplatz verschwenden. Also abgesehen von dem bisschen Platz den der Verzeichniseintrag braucht.
-
@Quiche-Lorraine @SeppJ Wahrscheinlich habe ich mich ungünstig ausgedrückt. Mir ging es darum, dass Kollisionsresitenz ein Merkmal von krypotografischen Hashfunktionen sind, aber nicht von Hashfunktionen im Allgemeinen.
zB. die einstellige Quersumme, ist eine Hashfunktion, aber natürlich weit weg von einer kryptorafischen Hashfunktion. U.a. ist es hier sehr einfach eine Kollision zu erzeugen.
-
@Schlangenmensch sagte in Mehrfach-Dateien erkennen C++ und std::filesystem:
@Quiche-Lorraine @SeppJ Wahrscheinlich habe ich mich ungünstig ausgedrückt. Mir ging es darum, dass Kollisionsresitenz ein Merkmal von krypotografischen Hashfunktionen sind, aber nicht von Hashfunktionen im Allgemeinen.
Resistenz gegen zufällige Kollisionen ist eine Eigenschaft jeder (guten) Hashfunktion. Natürlich nur soweit die Länge/der Wertebereich es zulässt. Was dagegen nicht für jede Hashfunktion erforderlich ist, ist Resistenz gegen Angriffe, wo man absichtlich versucht Kollisionen herbeizuführen.
zB. die einstellige Quersumme, ist eine Hashfunktion, aber natürlich weit weg von einer kryptorafischen Hashfunktion. U.a. ist es hier sehr einfach eine Kollision zu erzeugen.
Das ist ein unsinniges Beispiel. Das Problem ist hier nicht nur der Algorithmus, sondern bereits der Wertebereich. Den Wertebereich gross genug zu machen ist aber nicht genug. Es muss auch der Algorithmus derart sein, dass es trotz voller Kontrolle über die Inputs praktisch unmöglich ist zwei Inputs zu erzeugen die auf den selben Wert hashen.
-
@hustbaer Aber all das sind nicht zwangsläufig Eigenschaften von Hashfunktionen, sondern Eigenschaften die von speziellen Hashfunktionen für bestimmte Anwendungen gefordert werden.
Nehmen wir die Prüfsumme für eine IBAN (Module 97-10). Die ist 2 Stellig. Da wird ein 20 stelliger Wertebereich auf 2 Stellen reduziert. Das reicht um typische Vertipper zu erkennen, ist aber weit weg von dem, was ich unter Kollisionsresitenz verstehe.
-
@Schlangenmensch
Ich würde die IBAN Prüfsumme nicht als Hashfunktion bezeichnen.
-
Warum nicht?
Ich habe mal geschaut, was Wikipedia schreibt. Dort findet sich u.a. der Satz "Bei Prüfsummen verwendet man Hashwerte, um Übertragungsfehler zu erkennen". (Hervorhebung von mir)
Der Definition dort scheint das auch nicht zu widersprechen.
Dann kommt auf allerdings der Absatz "Kriterien" mit "Geringe Wahrscheinlichkeit von Kollisionen der Hashwerte für den Eingabewertebereich, also möglichst eine Gleichverteilung der Hashwerte auf die erwarteten Eingabewerte." Das klingt nach Wischi-Waschi und ich kann es aus der Definition dort nicht ableiten. Finde auch spannend, dass dort "Die Funktion muss schnell berechenbar sein" als Kriterium angegeben ist. Für Krypto kann man ja auch das Gegenteil wollen.
(nun ist Wikipedia aber sicher nicht der einzige Ort, wo es Definitionen von Hash-Funktionen gibt).
-
@wob sagte in Mehrfach-Dateien erkennen C++ und std::filesystem:
Warum nicht?
Der Zweck einer Prüfsumme ist es Fehler zu erkennen.
Der Zweck eines Hashcodes ist es Inputs gleichmässig auf Buckets zu verteilen.Es gibt natürlich viele Algorithmen die sich für beides gut eignen.
Und der Zweck der IBAN Prüfsumme ist halt Fehler zu erkennen. Weiters ist sie als Hashcode nur sehr begrenzt geeignet - eigentlich nur wenn man zufällig genau 97 Buckets verwenden will. Mehr Buckets geht sowieso nicht, und bei weniger Buckets muss man eine beträchtliche Ungleichverteilung in Kauf nehmen. Von daher macht die Bezeichnung als Hashcode mMn. keinen Sinn.
-
@SeppJ sagte in Mehrfach-Dateien erkennen C++ und std::filesystem:
Aber hoffentlich nicht alles auf einmal?
Upps... daran muss man arbeiten, hatte ich schon vermutet, das ist ja katastrophal langsam (und außerdem müsste man wohl Spezialfälle für große Dateien ebenfalls betrachten).
Das ist jetzt das Beste, was ich ohne Hashing und sonstigen Schnick-Schnack erzielen konnte, für den ad-hoc Dateivergleich:
template<std::streamsize BufSize> bool is_duplicate_file(const fs::path& a_path, const fs::path& b_path) { if (!fs::is_regular_file(a_path) || !fs::is_regular_file(b_path)) return false; if (fs::equivalent(a_path, b_path)) return true; if (fs::file_size(a_path) != fs::file_size(b_path)) return false; std::ifstream a_in{ a_path, std::ios::binary }; std::ifstream b_in{ b_path, std::ios::binary }; if (!a_in || !b_in) { // Exceptional case because files cannot be compared. Therefore, can neither return true nor false. throw std::runtime_error("is_duplicate_file: Unable to open file(s) for reading"); } if (a_in.peek() != b_in.peek()) return false; std::array<char, BufSize> a_buf{}; std::array<char, BufSize> b_buf{}; while (a_in.read(a_buf.data(), BufSize) && b_in.read(b_buf.data(), BufSize)) { if (a_buf != b_buf) return false; } if (!a_in.eof() || !b_in.eof()) { // Exceptional case because if this point is reached, given file sizes are equal, eventually while-loop would exit due to eof(). throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing"); } return true; }
-
Schaut mir fein aus
-
@SeppJ Ein Bug ist mir noch reingerutscht, ich weiß auch gar nicht, ob man so einen Mist verhindern kann (o.k., mit Tests halt, oder wahrscheinlich bessere Namensgebung, z. B. sollten
a
undb
eher bennant sein, um auf einen Pfad/Datei/... hinzudeuten, wiea_path
undb_path
):if (a != b) return false; // muss sein: if (a_buf != b_buf) return false;
Edit 1: Weiterer potenzieller Bug, durch die Konstruktion der
while-Schleife
wird beim letzten gültigen read bereitsa_in.eof()
und somit wird das letzteb.read()
nicht mehr erreicht, ist mir dadurch erst aufgefallen, dass dieif (!a_in.eof() || !b_in.eof())
Ausnahme ausgelöst wurde.while (a_in.read(a_buf.data(), BufSize) && b_in.read(b_buf.data(), BufSize)) // müsste evtl. sein: -- NEIN, das ist Quatsch, das ist wie while(true) while ((a_in.read(buf_a.data(), BufSize) || a_in.eof()) && (b_in.read(buf_b.data(), BufSize) || b_in.eof())) { } // müsste evtl. sein (das ist auch nicht optimal, wenn a::fail und b nicht, aber die Dateien in Wirklichkeit gleich wären, passt es nicht): do { a_in.read(buf_a.data(), BufSize); b_in.read(buf_b.data(), BufSize); if (buf_a != buf_b) return false; } while (a_in && b_in); // Oder so, und dann den äußeren if (!a_in.eof() || !b_in.eof()) weglassen (hässlich, aber gut?): do { a_in.read(buf_a.data(), BufSize); if (a_in.fail() || a_in.bad()) throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing"); b_in.read(buf_b.data(), BufSize); if (b_in.fail() || b_in.bad()) throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing"); if (buf_a != buf_b) return false; } while (a_in && b_in);
-
Argh, das hätte mir auffallen müssen. Zumal ich speziell auf das Verhalten in Fehlerfällen geachtet habe, das aber solide aussieht. Habe da wohl beim Lesen den Normalfall vernachlässigt
PS: Was hältst du von
a_in.read(…).rd-state() == b_in.read(…).rd_state() && a_in
?
PPS: Schaut so aus als hätte ein iostate keinen Vergleichsoperator . Cast zu bool dann? Deine Vorschläge finde ich jedenfalls ein bisschen schwer verständlich, bin mir gar nicht sicher, ob sie stimmen.
-
@SeppJ Meine Überlegung ist, dass das mit der while-Bedingung (und Prüfen des Rückgabewerts von read()) nicht funktioneren kann, weil
eof()
immer abbrechen würde. Also ich könnte damit leben:while (a_in && b_in) { a_in.read(buf_a.data(), BufSize); b_in.read(buf_b.data(), BufSize); if (a_in.fail() || a_in.bad() || b_in.fail() || b_in.bad()) { throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing"); } if (buf_a != buf_b) return false; }
Edit2: Ach manno: https://devdocs.io/cpp/io/ios_base/iostate
[...]
Note that in nearly all situations, if eofbit is set, the failbit is set as well.
[...]
The failbit is set by the following standard library functions:- basic_istream::read, if the end-of-file condition occurs on the input stream before all requested characters could be extracted.
Es scheint eigentlich so, dass realistisch bei read() nichts schiefgehen kann, sobald die Datei erfolgreich geöffnet wurde (was nicht auch das Programm abschießen würde, z. B. Stromausfall), außer dass die Dateien EOF sind. Vielleicht ist also dieser zweite Check mit
throw
overkill.Edit3: Noch ein allgemeiner Hinweis (oder Vermutung): Die filesystem Funktionen wie
file_size
oderis_regular_file
führen zu "syscalls" o. Ä. und unnötige Aufrufe sind unbedingt zu vermeiden / minimieren, sofern möglich. Ich hatte einen zusätzlichenfile_size
sowieis_regular_file
aufruf und dadurch gleich mal die Performance halbiert im Vergleich zur Funktion ohne die zusätzlichen Aufrufe. Wohingegenfs::equivalent
vernachlässigbar zu sein scheint. Die Verwendung vonpeek()
bringt nicht zuverlässig etwas, sodass sich die Zeile wohl nicht lohnt.
Wie man sieht, habe ich einiges ausprobiert:$ ./dupf.exe --benchmark 'D:/Pictures' Running [FileCompare_baseline] Total bytes compared : 0 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 2481.48ms Running [FileCompare_baseline_file_size] Total bytes compared : 0 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3515.58ms Running [FileCompare_baseline_is_regular_file] Total bytes compared : 0 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3534.22ms Running [FileCompare_bytewise_ignoreSize] Total bytes compared : 48502896 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 7210.14ms Running [FileCompare_bytewise_peek_ignoreSize] Total bytes compared : 48472646 Total files compared : 32950 Total size exits : 0 Total peek exits : 30264 Total seekg exits : 0 Total duplicates found : 0 Time : 7464.32ms Running [FileCompare_noHash<128ll>] Total bytes compared : 48453120 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3639.78ms Running [FileCompare_noHash_2<128ll>] Total bytes compared : 44416768 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 12 Time : 3642.16ms Running [FileCompare_noHash_2_ignoreSize<128ll>] Total bytes compared : 44759040 Total files compared : 32950 Total size exits : 0 Total peek exits : 30264 Total seekg exits : 0 Total duplicates found : 12 Time : 10373.1ms Running [FileCompare_bytewise_size] Total bytes compared : 48454500 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 4923.53ms Running [FileCompare_stdequal_ignoreSize<128ull>] Total bytes compared : 52627840 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 5880.31ms Running [FileCompare_memcmp_ignoreSize<128ull>] Total bytes compared : 52627840 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 5890.91ms Running [FileCompare_stdequal<128ull>] Total bytes compared : 48453120 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3618.82ms Running [FileCompare_memcmp<128ull>] Total bytes compared : 48453120 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3671.38ms Running [FileCompare_stdequal<2048ull>] Total bytes compared : 48439296 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3623.17ms Running [FileCompare_memcmp<2048ull>] Total bytes compared : 48439296 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3608.61ms Running [FileCompare_memcmp<16384ull>] Total bytes compared : 48365568 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3548.36ms Running [FileCompare_memcmp<65536ull>] Total bytes compared : 47972352 Total files compared : 12 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 3545.12ms Running [FileCompare_mmap] Total bytes compared : 48454488 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 12 Time : 7584.03ms Running [FileCompare_peek_end] Total bytes compared : 48454500 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 8355.01ms Running [FileCompare_xxHash] Total bytes compared : 48454488 Total files compared : 32950 Total size exits : 32938 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 12 Time : 3564.38ms Running [FileCompare_readall] Total bytes compared : 7533884225 Total files compared : 32950 Total size exits : 0 Total peek exits : 0 Total seekg exits : 0 Total duplicates found : 0 Time : 13140.2ms
Man sieht auch wie viele dieser "Benchmarks" hier tatsächlich fehlerbehaftet waren (weil "Total duplicates found : 0", obwohl es 12 sein müssen), wegen ähnlicher Probleme wie oben beschrieben, weil der eof() Fall beispielsweise nicht korrekt gehandhabt wird. Die Zahlen sind trotzdem einigermaßen nützlich für mich gewesen. Die oben beschriebene Funktion wird durch
FileCompare_noHash_2
verwendet.
-
Ich würde hier explizit
memcmp
verwenden für den Vergleich. Klar, man kann hoffen dassstd::array::operator ==
ne passende Spezialisierung hat oder der Compiler schlau genug ist die char-Vergleichs-Schleife zu nemmemcmp
zu optimieren. Aber man kann es auch direkt selbst hinschreiben und sich sicher seinWeitere Optimierung wird dann ... schwierig. Weil man dann unterscheiden müsste ob die Files vom selben Datenträger kommen oder nicht. Auf dem selben Datenträger wären sehr grosse Puffer von Vorteil und abwechselnd Lesen -- speziell wenn es HDDs sind. Bei unterschiedlichen Datenträgern ist meist ne kleinere Puffergrösse besser und gleichzeitig lesen (mit async IO oder zwei Threads).
Und dann kann man natürlich noch den Vergleich in einen eigenen Thread auslagern. Also zwei Sets an Puffern verwenden und dann gleichzeitig Lesen und Vergleichen.
Wobei es gut sein kann dass man mit passender Puffergrösse selbst mit der einfachen Variante (single threaded, synchron) in den meisten Fällen close-to-optimal Performance schaffen wird. Jedes vernünftige OS macht ja read-ahead. Man muss nur schauen dass man den read-ahead Mechanismus nicht ohne es zu wollen deaktiviert -- was AFAIK passiert wenn man zu grosse Puffer verwendet. 64 oder 128 kB sollte "safe" sein.
-
@hustbaer Danke für die zusätzlichen Hinweise.
Um das Thema im Rahmen zu halten war mein Ziel, ein einfach gestricktes "standard C++", single-threaded Programm ohne (explizit) OS-spezifische Funktionen zu verwenden, auch um einfachstd::filesystem
mal auszuloten aber auch überhaupt kennenzulernen. Das wird nicht "perfekt" sein bei weitem. Allein dieses "inode"-Thema habe ich bei anderer SW auch schon gesehen, und darauf hat man mitstd::filesystem
gar keinen Zugriff. D. h. ich habe eine einfache is_duplicate Funktion, das werde ich demnächst noch mit dem Rest verknüpfen und dann lass ich's erstmal gut sein.Ich hab ja von dem Ganzen nicht so viel Ahnung, aber ich interpretiere das so, dass GCC die schlaue memcmp Optimierung durchführt, man kann also eigentlich immer getrost
std::equal
verwenden (und std::array::operator== verwendet (hier) auchstd::equal
):// [gcc/libstdc++-v3/include/std/array] // ... // Array comparisons. template<typename _Tp, std::size_t _Nm> [[__nodiscard__]] _GLIBCXX20_CONSTEXPR inline bool operator==(const array<_Tp, _Nm>& __one, const array<_Tp, _Nm>& __two) { return std::equal(__one.begin(), __one.end(), __two.begin()); } // .. // [gcc/libstdc++-v3/include/bits/stl_algobase.h] template<bool _BoolType> struct __equal { template<typename _II1, typename _II2> _GLIBCXX20_CONSTEXPR static bool equal(_II1 __first1, _II1 __last1, _II2 __first2) { for (; __first1 != __last1; ++__first1, (void) ++__first2) if (!(*__first1 == *__first2)) return false; return true; } }; template<> struct __equal<true> { template<typename _Tp> _GLIBCXX20_CONSTEXPR static bool equal(const _Tp* __first1, const _Tp* __last1, const _Tp* __first2) { if (const size_t __len = (__last1 - __first1)) return !std::__memcmp(__first1, __first2, __len); return true; } }; // ... template<typename _II1, typename _II2> _GLIBCXX20_CONSTEXPR inline bool __equal_aux1(_II1 __first1, _II1 __last1, _II2 __first2) { typedef typename iterator_traits<_II1>::value_type _ValueType1; const bool __simple = ((__is_integer<_ValueType1>::__value || __is_pointer<_ValueType1>::__value) && __memcmpable<_II1, _II2>::__value); return std::__equal<__simple>::equal(__first1, __last1, __first2); } template<typename _II1, typename _II2> _GLIBCXX20_CONSTEXPR inline bool __equal_aux(_II1 __first1, _II1 __last1, _II2 __first2) { return std::__equal_aux1(std::__niter_base(__first1), std::__niter_base(__last1), std::__niter_base(__first2)); } // ... template<typename _II1, typename _II2> _GLIBCXX20_CONSTEXPR inline bool equal(_II1 __first1, _II1 __last1, _II2 __first2) { // concept requirements __glibcxx_function_requires(_InputIteratorConcept<_II1>) __glibcxx_function_requires(_InputIteratorConcept<_II2>) __glibcxx_function_requires(_EqualOpConcept< typename iterator_traits<_II1>::value_type, typename iterator_traits<_II2>::value_type>) __glibcxx_requires_can_increment_range(__first1, __last1, __first2); return std::__equal_aux(__first1, __last1, __first2); }
Aber ich sehe den Punkt, man kann ja auch einfach
<cstring>
undstd::memcmp
verwenden, warum nicht, ja.
-
Hier jetzt ein aktualisiertes Beispielprogramm:
#include <array> #include <cstring> #include <filesystem> #include <fstream> #include <iostream> #include <unordered_map> #include <vector> namespace fs = std::filesystem; static constexpr std::streamsize BufSize{ 4096 }; template<std::streamsize BufSize> bool is_duplicate_file(const fs::directory_entry& file_a, const fs::directory_entry& file_b) { if (!file_a.is_regular_file() || !file_b.is_regular_file()) { return false; } if (fs::equivalent(file_a, file_b)) { return true; } if (file_a.file_size() != file_b.file_size()) { return false; } std::ifstream a_in{ file_a.path(), std::ios::binary }; std::ifstream b_in{ file_b.path(), std::ios::binary }; if (!a_in || !b_in) { throw std::runtime_error("is_duplicate_file: Unable to open file(s) for reading"); } if (a_in.peek() != b_in.peek()) { return false; } std::array<char, BufSize> buf_a{}; std::array<char, BufSize> buf_b{}; while (a_in && b_in) { a_in.read(buf_a.data(), BufSize); b_in.read(buf_b.data(), BufSize); /* Seems superfluous since what can fail besides hitting EOF on ifstream::read(). if (a_in.fail() && !a_in.eof() || a_in.bad() || b_in.fail() && !b_in.eof() || b_in.bad()) { throw std::runtime_error("is_duplicate_file: Unexpected file read error while comparing"); } */ if (std::memcmp(buf_a.data(), buf_b.data(), BufSize)) return false; } return true; } auto build_file_index(const fs::path& start_path) { std::unordered_multimap<std::uintmax_t, fs::directory_entry> files_by_size; for (const auto& entry : fs::recursive_directory_iterator{ start_path }) { if (entry.is_regular_file()) { files_by_size.emplace(entry.file_size(), entry); } } return files_by_size; } auto find_duplicate_files(const auto& files) { std::unordered_map<fs::path, std::vector<fs::path>> dupes; for (auto entry = files.begin(); entry != files.end(); ) { auto range{ files.equal_range(entry->first) }; int idx_i{}; std::vector<bool> matched(std::distance(range.first, range.second), false); // std::unordered_map<fs::path, bool> works, too! for (auto i = range.first; i != range.second; ++i, ++idx_i) { int idx_j{ idx_i + 1 }; for (auto j = std::next(i); j != range.second; ++j, ++idx_j) { if (!matched[idx_i] && !matched[idx_j] && is_duplicate_file<BufSize>(i->second, j->second)) { dupes[i->second.path()].emplace_back(j->second.path()); matched[idx_j] = true; } } } entry = range.second; } return dupes; } void print_dupes(const auto& dupes) { for (const auto& a : dupes) { std::cout << "\nReference: " << a.first << '\n'; for (const auto& b : a.second) { std::cout << "Duplicate: " << b << '\n'; } } std::cout << "\nFound a total of " << dupes.size() << " duplicate groups.\n\n"; } int main(int argc, char* argv[]) try { if (argc != 2) { std::cout << "Program expects one argument: <Starting path>\n"; return 1; } fs::path start_path{ argv[1] }; if (!fs::exists(start_path)) { std::cout << "Provided argument is not an existing path.\n"; return 2; } print_dupes( find_duplicate_files( build_file_index(start_path))); } catch (const fs::filesystem_error& e) { std::cerr << e.what() << '\n'; return e.code().value(); } catch (const std::runtime_error& e) { std::cerr << e.what() << '\n'; return -2; } catch (...) { std::cerr << "Unhandled exception!\n"; return -1; }
Edit 1: Ich hatte beim Programmieren auch an
std::unordered_map<fs::path, bool>
gedacht, was sich performance-technisch nicht viel schenkt mitstd::vector<bool>
. Inzwischen wurde ich darauf hingewiesen, dass man auch einfachstd::unordered_set<fs::path>
verwenden könnte - auch von der Performance her ähnlich, aber wahrscheinlich eleganter als dieser Pfusch mit den Indizes.
Die Idee war klar, wenn z. B. Dateien A, B, C mit Größe X geprüft werden, und ich habe bereits erkannt, dass Datei B mit A identisch sind, dann brauche ich gar nicht erst versuchen, Datei B auch noch mit C zu vergleichen (das wäre sinnlos, wenn B==C, dann wäre C bereits gegen A gefunden worden).Edit 2:
Jetzt so:auto find_duplicate_files(const auto& files) { std::unordered_map<fs::path, std::vector<fs::path>> duplicates; std::unordered_set<fs::path> matched; for (auto left = files.begin(); left != files.end(); ) { auto range = files.equal_range(left->first); left = range.second; for (auto ref_it = range.first; ref_it != range.second; ++ref_it) { if (matched.contains(ref_it->second.path())) { continue; } for (auto file_it = std::next(ref_it); file_it != range.second; ++file_it) { if (!matched.contains(file_it->second.path()) && is_duplicate_file<BufSize>(ref_it->second, file_it->second)) { matched.emplace(file_it->second.path()); duplicates[ref_it->second.path()].emplace_back(file_it->second.path()); } } } } return duplicates; }