Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Autor: Grzegorz 'baziorek' Bazior
Inne artykuły

[C/C++] Tworzenie diffów między dwoma plikami za pomocą biblioteki librsync

[artykuł] Artykuł opisuje bibliotekę librsync do tworzenia diffów między dwoma plikami bez konieczności posiadania obydwu plików, dająca możliwość odtwarzania plików w oparciu o diffy.

Intro - problem do rozwiązania

Jesteś programistą, więc na pewno znasz jakieś narzędzie do porównywania plików np. narzędzie diff. Możesz chcieć sprawdzić co się zmieniło w danej wersji pliku w stosunku do jego poprzedniej wersji lub jeszcze wcześniejszej. Na pierwszy rzut oka narzuca się pomysł na trzymanie każdej wersji pliku -to rozwiązanie jest bardzo pamięciożerne, gdyż często w dużym pliku (np. dużym pliku archiwum ZIP) może się zmienić tylko drobiazg (np. komentarz archiwum). Jak widać trzymanie całych wersji jest bardzo nieefektywne, co dopiero wysyłanie całych wersji przez sieć. Ewentualnie możemy trzymać diffy każdego pliku, ale co w przypadku plików binarnych?

Zaproponowane rozwiązanie

Każdy plik jest dzielony na kawałki, dla każdego kawałka tworzona jest checksuma, właśnie te checksumy są porównywane i w razie niezgodności tworzony jest diff (określony w tej bibliotece jako delta) z kawałków o niezgodnej checksumie (więcej informacji na temat algorytmu). W odróżnieniu od innych bibliotek generujących różnicę między wersjami pliku zamiast trzymania dwóch wersji pliku wystarczy jeden oraz plik checksum każdego bloku w pliku (określony w tej bibliotece jako sygnatura).

Właśnie takie rozwiązanie stosuje dość popularny program rsync służący do synchronizacji plików przez sieć. Do generowania diffów plików i stosowania ich celem doprowadzenia do identyczności plików w różnych wersjach służy biblioteka librsync używana przez programy rsync, dropbox, rdiff-backup i inne narzędzia.

Nazwa librsync się kojarzy z rsync, ale biblioteka ta nie dostarcza wszystkich mechanizmów programu rsync takich jak synchronizacja całych katalogów, serwer, synchronizacja przez sieć itp., dostarcza funkcjonalność w zasadniczo trzech operacjach:
  • generowanie sygnatury pliku A
  • tworzenie delty z pliku sygnatury i pliku B
  • zastosowywanie zmian, zawartych w pliku delty do pliku A celem odtworzenia pliku B
W plikach biblioteki poza biblioteką jest dostarczone narzędzie rdiff, które z poziomu wiersza poleceń dostarcza powyższe operacje.

Jak pobrać

Tutaj pojawia się pewien dylemat -zależy którą wersję biblioteki, a wybór nie jest taki trywialny:
  • jest stara wersja 0.9.7, do pobrania na https://sourceforge.net​/projects/librsync/, którą łatwo się instaluje dla Linuxa (skrypt
    ./configure
    ), na Windowsie przy użyciu http://mingw.org/wiki/msys również możemy zbudować w podobny sposób jak na Linuxie. Jednak, jeśli ktoś nie chce użyć MSYS można dzięki paru modyfikacjom w kodzie zbudować na dowolnym środowisku programistycznym np. na QT Creatorze przy użyciu wbudowanego MinGW (za chwilę to opiszę).
  • wersja 2.0.1, do pobrania na https://github.com/librsync​/librsync, z której skrypt
    ./configure
     został zastąpiony CMake'iem, lecz niestety bez obsługi przydatnych opcji, poza tym potrzeba nam ściągnąć dodatkowe biblioteki i narzędzia, które w powyższej były dostarczone lub zbędne
  • dla dociekliwych jest jeszcze sprawdzona wersja zmodyfikowana przez dropboxa, do pobrania na https://github.com/dropbox​/librsync, dla której nie zadziałają poniższe przykłady, a także narzędzie rdiff jest zaniedbane, jej zaletą jest to, iż ma dostarczone projekty dla Visual Studio oraz zrefaktorowany kod oryginalnej wersji biblioteki z pewnymi modyfikacjami.
W tym punkcie można dodać, że na stronie domowej jest na dzień dzisiejszy podana licencja GNU LGPL v2.1.

Instalacja starej wersji 0.9.7

Budowa tej wersji biblioteki jest bardzo prosta, a zarazem mające więcej funkcji konfiguracyjnych

Budowa Linux

Tutaj opis jest zbędny, wystarczy:
./configure
make
sudo make install
Możemy w skrypcie
./configure
 opcjonalnie wskazać miejsce instalacji (
--prefix=...
) lub ustawić, aby były tworzone dynamiczne biblioteki zamiast/oprócz statycznych (szczegóły
./configure --help
).

Budowa Windows z użyciem MSYS

Jeśli ktoś dobrze skonfiguruje sobie http://mingw.org/wiki/msys (czyli zainstaluje make, dobrze poda ścieżki do g++) to budowa przebiega podobnie jak dla Linuxa.

Budowa Windows bez użycia MSYS

Teraz pytanie, co chcemy zbudować, jeśli tylko bibliotekę librsync to dodajemy w naszym środowisku (np. QT Creatorze) wszystkie pliki języka C dostarczone przez bibliotekę, z wyjątkiem plików zawierających funkcję main() (isprefix.driver.c oraz rdiff.c).
Jeśli natomiast chcemy zbudować narzędzie rdiff (co to jest to za chwilę) to jednak musimy dodać do projektu również plik rdiff.c.
Następnie musimy do ścieżki includów dodać również katalog PCbuild (jest tam plik config.h), a jeśli budujemy rdiffa to też katalog popt.
Niestety są również wymagana jest pewna modyfikacje w kodzie (oczywiście lepiej to poprawiać dopiero w sytuacji, gdy kompilator na to nam zwróci uwagę), a mianowicie w pliku delta.c należy usunąć przedrostek inline z deklaracji funkcji (lub zamienić inline na static inline).
Do pełnego skompilowania brakuje nam jeszcze dodania stałych czasu kompilacji HAVE_STDARG_H i HAVE_STRERROR.

Budowa Windows na Visual Studio

Na VS 2015 musiałem zrobić te same kroki co powyżej, bez modyfikacji pliku delta.c. Musiałem również dodać dodatkowe stałe czasu kompilacji: _CRT_SECURE_NO_WARNINGS i _CRT_NONSTDC_NO_WARNINGS; oczywiście zamiast wyłączać ostrzeżenia można poprawić kod, aby używał bezpieczniejszych funkcji.

Instalacja najnowszej wersji 2.0.1

Niestety w kolejnej wersji jest ciężej to wszystko zbudować na Linuxie, a co dopiero na Windowsie. Bibliotekę w nowej wersji można pobrać z githuba.
Do zbudowania potrzebujemy mieć perla (może być wersja portable).

Budowa Linux

Jeśli chcemy zbudować również rdiff to musimy doinstalować bibliotekę popt.
Tutaj nie jest dostarczony instalator configure, zamiast tego mamy cmake, dlatego budujemy:
cmake .
make
sudo make install
możemy jeszcze zmienić na linkowanie statyczne bibliotek:
sed -i 's/SHARED/STATIC/g' CMakeLists.txt

możemy też ustawić ścieżkę do budowania naszej biblioteki
cmake -DCMAKE_INSTALL_PREFIX:PATH=/gdzie/budujemy/biblioteke/ .

Budowa Windows

Samo zbudowanie biblioteki przy pomocy CMake'a nie jest takie trudne. Wpierw należy poustawiać ścieżki zmiennej środowiskowej Path (do kompilatora, Perla, CMake'a) i zrestartować komputer. Teraz wystarczy w rozpakowanym katalogu biblioteki wywołać:
PS C:\Users\user\Desktop\librsync\librsync-master> cmake -G "[konfiguracja]" --target=rsync .
PS C:\Users\user\Desktop\librsync\librsync-master> # komenda budowy...
Komenda cmake dla MinGW z podaniem ścieżki budowy biblioteki może wyglądać tak:
PS C:\Users\user\Desktop\librsync\librsync-master> cmake -G "MinGW Makefiles" -DCMAKE_INSTALL_PREFIX:PATH=./compiled/ --target=rsync .
PS C:\Users\user\Desktop\librsync\librsync-master> mingw32-make.exe
PS C:\Users\user\Desktop\librsync\librsync-master> mingw32-make.exe install
Możemy też zmienić sposób budowania biblioteki, aby mieć skompilowaną bibliotekę statycznie zamiast dynamicznie, w tym celu zmieniamy w pliku CMakeLists.txt tekst SHARED na STATIC.
Uwaga: jeśli chcemy więcej niż raz zawołać w tym samym katalogu komendę cmake to musimy usunąć cache CMake'a (plik CMakeCache.txt) przed kolejnym wywołaniem.
Jeśli natomiast chcielibyśmy zbudować sobie narzędzie rdiff to musimy jeszcze pobrać pewne dodatkowe biblioteki.

Narzędzie rdiff

Jest narzędziem dostarczonym wraz z bibliotekę librsync demonstrującym to, co dostarcza owa biblioteka. Opiszę je, gdyż pomoże to w zrozumieniu dostarczonych funkcji, z których to korzysta narzędzie rdiff.
Zasadniczo narzędzie ma trzy funkcje:
  • rdiff signature - tworzy sygnaturę dla pliku (powiedzmy w wersji 1.0), sygnatura-plik zawierający informacje-hashe dla każdego z bloków pliku.
  • rdiff delta - tworzy deltę zmian (diffa), w oparciu o sygnaturę (powiedzmy wersji 1.0) oraz plik w wyższej wersji (powiedzmy 2.0). Deltę możemy wysłać komuś kto posiada plik w niższej wersji (czyli powiedzmy tej 1.0), dzięki temu będzie on mógł zaktualizować swój plik w starszej wersji (powiedzmy 1.0) do nowszej (powiedzmy 2.0) w oparciu o samą deltę.
  • rdiff patch - mając deltę pliku nowszej wersji (powiedzmy 2.0) oraz plik w starszej wersji (powiedzmy 1.0) tworzy nam plik w nowszej wersji (powiedzmy 2.0).

Ten opis wygląda zawile, więc podam prosty przykład praktyczny:
Student ma dostarczoną płytkę z pracą magisterską do promotora i sekretariatu, termin obrony za miesiąc, więc wyjechał za granicę. Nagle promotor zauważył plagiat w kodzie programu i w tekście, nie chcąc, aby jego podopieczny miał do końca życia zniszczoną karierę naukową dzwoni do niego i każe podmienić płytę na niezawierającą plagiatów.
Niestety student jest za granicą, więc nie podmieni płytki po wprowadzeniu poprawek, promotor się zgodził podmienić za studenta, ale nie da się wysłać całej płytki przez internet, gdyż tam za odległą granicą nie ma kafejek internetowych, internet jest tylko mobilny, ale bardzo drogi i wolny, plików do modyfikacji jest pare (a dokument tekstowy jest bardzo duży). Wydaje się, że student nie jest w stanie obronić swojej przyszłości, ale doświadczony promotor nawiązuje rozmowę:
Promotor: A masz u siebie na dysku obraz płyty oddanej do sekretariatu?
Student: Mam, mogę do niego coś dodać, ale nie dam rady go przesłać przez internet w całości, nawet nie dam rady przesłać wszystkich zmodyfikowanych plików przez internet
Promotor: Mam rozwiązanie - użyj rdiffa - z obrazu płyty oddanej do sekretariatu zrób sygnaturę, następnie wprowadź swoje zmiany do obrazu płyty, następnie wyślij mi deltę zrobioną ze zmienionego obrazu i sygnatury obrazu złożonej płyty.
Promotor wtedy mając deltę aktualizuje plik obrazu do najnowszej wersji, nagrywa i podmienia w sekretariacie.

Przykład użycia programu rdiff

Zacznę od tego, że poniższe pliki to nie wyżej wymieniona praca magisterska. W poniższych komendach dla pliku w wersji 1.0 utworzę sygnaturę, następnie w oparciu o tę sygnaturę i plik w wersji 2.0 utworzę deltę, która posłuży za odtworzenie pliku w wersji 2.0 z pliku w wersji 1.0:
$ rdiff signature project_v1.0.zip       project_v1.0.signature
$ rdiff delta     project_v1.0.signature project_v2.0.zip project_v1.0_v2.0.delta
$ rdiff patch     project_v1.0.zip       project_v1.0_v2.0.delta project_v2.0_recreated.zip
$ ls -lh
-rw-r--r--  1 grzegorz grzegorz 3,2K lip 11 00:09 project_v1.0.signature
-rw-r--r--  1 grzegorz grzegorz 215K lip 11 00:10 project_v1.0_v2.0.delta
-rw-r--r--  1 grzegorz grzegorz 537K lip 10 20:53 project_v1.0.zip
-rw-r--r--  1 grzegorz grzegorz 751K lip 10 23:56 project_v2.0.zip
-rw-r--r--  1 grzegorz grzegorz 751K lip 11 00:11 project_v2.0_recreated.zip
Możemy nie podawać nazwy pliku wyjściowego, ale wtedy wyjście zostanie wypisane na ekran w formacie binarnym.

Argumenty uruchomienia programu rdiff

Oto opisane opcje uruchomienia programu rdiff:
$ rdiff -h
Usage: rdiff [OPTIONS] signature [BASIS [SIGNATURE]]
             [OPTIONS] delta SIGNATURE [NEWFILE [DELTA]]
             [OPTIONS] patch BASIS [DELTA [NEWFILE]]

Options:
  -v, --verbose             Trace internal processing
  -V, --version             Show program version
  -?, --help                Show this help message
  -s, --statistics          Show performance statistics
Delta-encoding options:
  -b, --block-size=BYTES    Signature block size
  -S, --sum-size=BYTES      Set signature strength
      --paranoia            Verify all rolling checksums
IO options:
  -I, --input-size=BYTES    Input buffer size
  -O, --output-size=BYTES   Output buffer size
  -z, --gzip[=LEVEL]        gzip-compress deltas
  -i, --bzip2[=LEVEL]       bzip2-compress deltas
jak widać z nich jest możliwość wyświetlenia statystyk, ich włączenie ubogaca nasz output w sposób podobny do poniższego:
$ rdiff -s signature project_v1.0.zip project_v1.0.signature
rdiff: signature statistics: signature[269 blocks, 2048 bytes per block]
$ rdiff -s delta project_v1.0.signature project_v2.0.zip project_v1.0_v2.0.delta
rdiff: loadsig statistics: signature[269 blocks, 2048 bytes per block]
rdiff: delta statistics: literal[16 cmds, 219529 bytes, 48 cmdbytes] copy[3 cmds, 548864 bytes, 0 false, 20 cmdbytes]
$ rdiff -s patch project_v1.0.zip project_v1.0_v2.0.delta project_v2.0_recreated.zip
rdiff: patch statistics: literal[16 cmds, 219529 bytes, 48 cmdbytes] copy[3 cmds, 548864 bytes, 0 false, 20 cmdbytes]
Jeśli chodzi o resztę argumentów urachamiania nie są jeszcze zaimplementowane opcje kompresji.

Delta pliku bez zmian

Jeśli sprawdzimy np:
$ diff plik.txt plik.txt
 to nie zostanie nic wyświetlone, sprawdźmy więc rozmiar delty pliku bez zmian:
$ cp project_v2.0.zip project_v3.0.zip
$ rdiff signature project_v2.0.zip project_v2.0.signature
$ rdiff delta project_v2.0.signature project_v3.0.zip project_v2.0_v3.0.delta
$ ll -h
-rw-r--r--  1 grzegorz grzegorz 4,5K lip 11 00:13 project_v2.0.signature
-rw-r--r--  1 grzegorz grzegorz   11 lip 11 00:14 project_v2.0_v3.0.delta
-rw-r--r--  1 grzegorz grzegorz 751K lip 10 23:56 project_v2.0.zip
-rw-r--r--  1 grzegorz grzegorz 751K lip 11 00:13 project_v3.0.zip
jak widać delta, gdy brak zmian, zajmuje więcej niż 0 bajtów, na szczęście jednak nie jest to dużo.

Użycie biblioteki librsync

Wiemy już co biblioteka robi, w jakiej kolejności należy wykonać pewne kroki, jakie daje korzyści. Więc jeśli komuś nie wystarczy narzędzie rdiff (chce używać tych funkcji w kodzie programu) to czas na ich opisanie.

Funkcje operujące na całych plikach

Biblioteka dostarcza funkcje operujące na całych plikach, opis tych funkcji znajduje się na odpowiedniej stronie dokumentacji, pozwolę sobie przetłumaczyć i zacytować dokumentację z powyższej strony zmieniając to w bardziej intuicyjną formę:
C/C++
// funkcja tworząca plik sygnatury (wersja 0.9.7):
rs_result rs_sig_file( FILE * plikOtwartyDoOdczytu, FILE * plikDoUtworzeniaSygnaturyOtwartyDoZapisu, size_t wielkoscBlokuPodczasGenerowaniaSygnatury, size_t przycietaDlugoscChecksumWBajtach, rs_stats_t * opcjonalnyWskaznikDoStatystyk );

// funkcja tworząca plik sygnatury (wersja 2.0.1):
rs_result rs_sig_file( FILE * plikOtwartyDoOdczytu, FILE * plikDoUtworzeniaSygnaturyOtwartyDoZapisu, size_t wielkoscBlokuPodczasGenerowaniaSygnatury, size_t przycietaDlugoscChecksumWBajtach, rs_magic_number numerOznaczajacyJakiFormatUzyc, rs_stats_t * opcjonalnyWskaznikDoStatystyk );

// funkcja ładująca plik sygnatury do pamięci (UWAGA: Patrz też na funkcję poniżej tej):
rs_result rs_loadsig_file( FILE * plikSygnatury, rs_signature_t ** sygnaturaWPamieci, rs_stats_t * statystyki );

// funkcja, którą trzeba zawołać celem zaindeksowania załadowanej sygnatury po jej załadowaniu
rs_result rs_build_hash_table( rs_signature_t * checkSumy );

// funkcja generująca plik różnic (delty) w oparciu o sygnaturę oraz nową wersję pliku:
rs_result rs_delta_file( rs_signature_t * sygnaturaWPamieci, FILE * nowaWersjaPliku, FILE * plikDelty, rs_stats_t * statystyki );

// funkcja odtwarzająca nowszą wersję pliku (np. 2.0) z pliku w poprzedniej wersji (np. 1.0) i delty:
rs_result rs_patch_file( FILE * plikWPoprzedniejWersji, FILE * plikDeltyMiedzyWersjami, FILE * wyjsciowyNowyPlik, rs_stats_t * statystyki );

Funkcje pomocnicze

Zanim przejdę do przykładów pozwolę sobie przedstawić pewne funkcje pomocnicze, z których będę korzystał w następnych przykładach:
C/C++
#include <memory>
using namespace std;

#include <cstdio>
#include <cstring>

shared_ptr < FILE > openFile( const char * fileName, const char * fileMode ) // throw (invalid_argument, runtime_error)
{
    if( nullptr == fileName || 0 == strlen( fileName ) )
    {
        throw invalid_argument( "File name can't be empty!" );
    }
   
    shared_ptr < FILE > file( fopen( fileName, fileMode ), fclose );
    if( !file )
    {
        throw runtime_error( string( "File " ) + fileName + " can't be opened in mode " + fileMode + "!" );
    }
    return file;
}

Przykład generowania delty

Czas na przykład użycia powyższych funkcji. Wraz z biblioteką dostarczonym przykładem jest kod rdiff.c, oczywiście nie będę przeklejał tego kodu, ani pisał rdiffa na swój sposób od początku, zamiast tego umieszczę parę przykładów funkcji bardziej na kształt C++.
Zacznijmy od funkcji generujących sygnaturę pliku oraz generującą deltę.
Jednak zanim pojawi się przykład na generowanie sygnatury podkreślam, iż funkcja rs_sig_file() między wersjami różni się ilością argumentów. Mamy też problem taki, iż dla "magicznego numeru" RS_MD4_SIG_MAGIC jedyna akceptowalna STRONG_LEN to 8, tego problemu nie mamy w przypadku RS_BLAKE2_SIG_MAGIC.
Innymi słowy dla wersji biblioteki 0.9.7 zadziała nam zawołanie:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, & stats );
Dla wersji 2+ jeśli chcemy kompatybilności z wersją 0.9.7 powinniśmy zawołać:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, 8, RS_MD4_SIG_MAGIC, & stats );
Dla wersji 2+ powinniśmy zawołać:
rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC, & stats );

Powyższe warunki odzwierciedliłem w postaci wymyślonych przeze mnie makr, których niestety biblioteka nie dostarcza:
C/C++
#define LIBRSYNC_VERSION 2
#define LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION

A oto obiecane przykłady:
C/C++
#include <iostream>
#include <memory>
using namespace std;

#include <cstdio>
#include <cstring>

#include <librsync.h>

// rdiff [fileName]  [signatureFileName]
void generateSignatureForFile( const char * fileName, const char * signatureFileName )
{
    shared_ptr < FILE > fileToGenerateSignatureFor = openFile( fileName, "rb" );
    shared_ptr < FILE > signatureFile = openFile( signatureFileName, "wb" );
   
    rs_stats_t stats;
    #if LIBRSYNC_VERSION>=2
    #ifdef LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION
    #define RS_DEFAULT_STRONG_LEN 8
    rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, RS_MD4_SIG_MAGIC, & stats );
    #else
    rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC, & stats );
    #endif
    #else
    rs_result result = rs_sig_file( fileToGenerateSignatureFor.get(), signatureFile.get(), RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, & stats );
    #endif
   
    if( RS_DONE != result )
    {
        throw runtime_error( "Error during generation of signature! " + string( rs_strerror( result ) ) );
    }
   
    rs_log_stats( & stats );
}

shared_ptr < rs_signature_t > loadSumsetIntoMemory( const char * signatureFileName )
{
    shared_ptr < FILE > signatureFile = openFile( signatureFileName, "rb" );
   
    rs_signature_t * sumsetRaw;
    rs_stats_t stats;
    rs_result result = rs_loadsig_file( signatureFile.get(), & sumsetRaw, & stats );
    shared_ptr < rs_signature_t > sumset( sumsetRaw, rs_free_sumset );
    if( RS_DONE != result )
    {
        throw runtime_error( "Problem during loading signature file to memory! " + string( rs_strerror( result ) ) );
    }
    rs_log_stats( & stats );
   
    result = rs_build_hash_table( sumset.get() );
    if( RS_DONE != result )
    {
        throw runtime_error( "Problem during creating hash table! " + string( rs_strerror( result ) ) );
    }
   
    return sumset;
}

// rdiff delta  [signatureFileName]  [newFileFileName]  [deltaFileName]
void generateDeltaFile( const char * signatureFileName, const char * newFileFileName, const char * deltaFileName )
{
    shared_ptr < rs_signature_t > sumset = loadSumsetIntoMemory( signatureFileName );
   
    shared_ptr < FILE > newFile = openFile( newFileFileName, "rb" );
    shared_ptr < FILE > deltaFile = openFile( deltaFileName, "wb" );
   
    rs_stats_t stats;
    rs_result result = rs_delta_file( sumset.get(), newFile.get(), deltaFile.get(), & stats );
    if( RS_DONE != result )
    {
        throw runtime_error( "Problem during creating delta file!" + string( rs_strerror( result ) ) );
    }
   
    rs_log_stats( & stats );
}

Mając powyższe funkcje możemy wygenerować deltę między dwoma plikami:
C/C++
string createDeltaFileReturningItsName( const char * file, const char * fileInDifferentVersion, bool removeSignatureFile = true )
{
    const string && signatureFileName = string( file ) + ".signature";
    generateSignatureForFile( file, signatureFileName.c_str() );
   
    const string && deltaFileName = string( file ) + '_' + fileInDifferentVersion + ".delta";
    generateDeltaFile( signatureFileName.c_str(), fileInDifferentVersion, deltaFileName.c_str() );
   
    if( removeSignatureFile )
         remove( signatureFileName.c_str() );
   
    return deltaFileName;
}
W powyższych funkcjach, z powodu użycia wyjątków, a chcąc mimo wszystko zagwarantować zwalnianie zasobów używam https://pl.wikipedia.org/wiki​/Resource_Acquisition_Is_Initialization.

Zostało nam jeszcze odtworzenie pliku w kolejnej wersji mając poprzednią wersję oraz deltę, oto przykładowa funkcja-gotowiec:
C/C++
#include <memory>
using namespace std;

#include <cstdio>

#include <librsync.h>

// rdiff patch     [basicFileName]       [deltaFileName]    [recreatedNewFileName]
void recreateFile( const char * basicFileName, const char * deltaFileName, const char * recreatedNewFileName )
{
    shared_ptr < FILE > basicFile = openFile( basicFileName, "rb" );
    shared_ptr < FILE > deltaFile = openFile( deltaFileName, "rb" );
    shared_ptr < FILE > recreatedNewFile = openFile( recreatedNewFileName, "wb" );
   
    rs_stats_t stats;
   
    rs_result result = rs_patch_file( basicFile.get(), deltaFile.get(), recreatedNewFile.get(), & stats );
    if( RS_DONE != result )
    {
        throw runtime_error( "Problem during patching file '" + string( recreatedNewFileName ) + "'! " + string( rs_strerror( result ) ) );
    }
   
    rs_log_stats( & stats );
}

Dodatkowe możliwości biblioteki rsync

Wszystkie publiczne funkcje biblioteki opisane są w dokumentacji, niemniej jednak skupię się na paru z nich:

Statystyki

Do trzymania statystyk jest struktura rs_stats_t. Możemy je wyświetlić przy użyciu funkcji:
C/C++
// funkcja formatująca statystyki do buffora:
char * rs_format_stats( rs_stats_t const * stats, char * buf, size_t size );
// funkcja wyświetlająca statystyki (używa ona powyższej funkcji):
int rs_log_stats( rs_stats_t const * stats );
obydwie funkcje generują tekst podobny do:
loadsig statistics: signature[269 blocks, 2048 bytes per block]

Jeśli chcielibyśmy inaczej wyświetlać statystyki musimy zagłębić się w to, co zawiera struktura rs_stats_t, na chwilę obecną w bibliotece nie są dostarczone inne funkcje do wyświetlania statystyk w inny sposób.
Jeśli nie interesują nas ani trochę statystyki, możemy do powyższych funkcji zamiast adresu struktury podawać nullptr.

Kody błędów i logowanie

Zacznijmy od włączenia logowania pracy biblioteki i błędów, służy do tego funkcja:
void rs_trace_set_level( rs_loglevel level );

Gdzie listę kodów błędów możemy znaleźć w dokumentacji, np. możemy wyświetlać każde wywołanie funkcji bibliotecznej podając wartość RS_LOG_DEBUG.

Mamy również możliwość ustawić funkcję zwrotną w razie błędu:
C/C++
typedef void rs_trace_fn_t( rs_loglevel level, char const * msg );

void rs_trace_to( rs_trace_fn_t * new_impl )

// domyślna funkcja zwrotna:
void rs_trace_stderr( rs_loglevel level, char const * msg );

// przed włączaniem śledzenia wywołań funkcji RS_LOG_DEBUG dobrze jest sprawdzić czy nie wyłączyliśmy tego przy kompilacji
int rs_supports_trace();

Rezultat operacji

Wiele z operacji zwraca: rs_result, możliwe opcje można zobaczyć w dokumentacji, natomiast do wyświetlenia błędu biblioteki librsync w formie czytelnej dla człowieka służy funkcja:
char const * rs_strerror( rs_result r );

Wyświetlanie wersji i treści licencji

Co ciekawe biblioteka ta zawiera zmienne globalne posiadające takie informacje:
C/C++
char const rs_librsync_version[];
char const rs_licence_string[];

Przetwarzanie strumieniowe

Funkcje do operowania na całych plikach są bardzo wygodne w użyciu, natomiast może się zdarzyć, że nie będziemy mieli do czynienia z plikami, a ze strumieniem danych (np. operując na bazie danych czy plikach zdalnych), wtedy nie musimy tworzyć na chwilę pliku, czytać go, itd., gdyż mamy też dostarczone w ramach tej biblioteki funkcje do obsługi strumieniowej. Zanim ich użyjemy musimy wypełnić strukturę rs_buffers_t, następnie rozpocząć daną operację, odbywa się to przy użyciu funkcji:
C/C++
rs_job_t * rs_sig_begin( size_t new_block_len, size_t strong_sum_len, rs_magic_number sig_magic ); // wersja 2.0.1
rs_job_t * rs_sig_begin( size_t new_block_len, size_t strong_sum_len ); // wersja 0.9.7
rs_job_t * rs_loadsig_begin( rs_signature_t ** );
rs_job_t * rs_delta_begin( rs_signature_t * );
rs_job_t * rs_patch_begin( rs_copy_cb * copy_cb, void * copy_arg );

Mając zapoczątkowaną operację, czyli wypełnioną strukturę rs_job_t, możemy iterować po buforach:
C/C++
rs_result rs_job_iter( rs_job_t *, rs_buffers_t * );

Przykłady operowania na strumieniach

Teraz czas na przykłady, tutaj będę umieszczał jeden przykład dla każdej operacji.
Poniższe przykłady otrzymałem drogą eksperymentów w oparciu o dokumentację i kod źródłowy biblioteki, nie znalazłem w internecie żadnych przykładów na generowanie przy użyciu strumieni, dlatego zastrzegam sobie, że kod poniższych funkcji może zawierać lekkie niedociągnięcia, które nie wyszły podczas moich testów na plikach, na których testowałem.

Zacznę od funkcji, których będę używał w kolejnych przykładach:
C/C++
#include <string>
#include <fstream>

using namespace std;

#include <librsync.h>


string getFileContext( const char * fileName )
{
    if( nullptr == fileName || 0 == strlen( fileName ) )
    {
        throw invalid_argument( "File name can't be empty!" );
    }
   
    ifstream fileToRead( fileName, ios::binary | ios::in );
    if( !fileToRead )
    {
        throw runtime_error( string( "File " ) + fileName + " can't be opened!" );
    }
   
    fileToRead.seekg( 0, fileToRead.end );
    const int fileLength = fileToRead.tellg();
    fileToRead.seekg( 0, fileToRead.beg );
   
    string fileContext( fileLength, '\0' );
    fileToRead.read( & fileContext[ 0 ], fileLength );
   
    return fileContext;
}

rs_buffers_t fillBuffers( const string & inputData, string & outputBuffer, bool noMoreDataAfterThat = true )
{
    rs_buffers_t buffers;
    buffers.next_in = const_cast < char *>( inputData.c_str() );
    buffers.avail_in = inputData.size();
    buffers.eof_in = noMoreDataAfterThat;
    buffers.next_out = const_cast < char *>( outputBuffer.c_str() );
    buffers.avail_out = outputBuffer.size();
    return buffers;
}
Wiem, że w powyższym przykładzie używam niepopularnego const_cast, niestety muszę tutaj przypisać na wskaźniki char*, oczywiście mógłbym samodzielnie alokować taki bufor, ale ze względu na używanie wyjątków, istniałaby możliwość wycieku pamięci, dlatego wolę używania jako bufora obiektów string.

Tworzenie sygnatury dla danych

W tej sytuacji również mamy rozbieżność w funkcji rs_sig_begin() dla różnych wersji biblioteki, dlatego używam, wymyślonych przeze mnie, makr analogicznie jak wyżej.
Tutaj również zacznę od przykładu, opis kontrowersyjnych fragmentów poniżej:
C/C++
#include <string>
#include <memory>

using namespace std;

#include <librsync.h>

string generateSignatureForData( const string & data )
{
    string outputBuffer( 2 * data.size(), '\0' );
   
    rs_buffers_t buffers = fillBuffers( data, outputBuffer );
   
    #if LIBRSYNC_VERSION>=2
    #ifdef LIBRSYNC_COMPATIBLE_WITH_OLD_VERSION
    #define RS_DEFAULT_STRONG_LEN 8
    shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN, RS_MD4_SIG_MAGIC ), rs_job_free );
    #else
    shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_MAX_STRONG_SUM_LENGTH, RS_BLAKE2_SIG_MAGIC ), rs_job_free );
    #endif
    #else
    shared_ptr < rs_job_t > job( rs_sig_begin( RS_DEFAULT_BLOCK_LEN, RS_DEFAULT_STRONG_LEN ), rs_job_free );
    #endif
   
    rs_result result = rs_job_iter( job.get(), & buffers );
   
    if( buffers.avail_in )
    {
        throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
    }
   
    if( RS_BLOCKED == result )
    {
        throw runtime_error( "Data stream not ended!" );
    }
    else if( RS_DONE != result )
    {
        throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
    }
   
    outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
   
    rs_log_stats( rs_job_statistics( job.get() ) );
   
    return outputBuffer;
}
W powyższym przykładzie używam wcześniej przedstawionych funkcji. Sprawdzenia błędów odbywają się zgodnie z dokumentacją. Rozmiar buffora wyjściowego robię w trochę trywialny sposób, który jednak może być podatny na błędy, czyli w razie gdyby buffor był zbyt maly to wejdziemy do pierwszego ifa, w tej sytuacji powinniśmy dostarczyć większy buffor i ponowić operację.
Bufor wyjściowy (nasza sygnatura pliku) jest zawarta w buforze na pozycji od początku buffora wejściowego do buffers.next_out, dlatego czyszczę wszystko poza tym. Żeby wyświetlić statystyk używam funkcji jak wyżej (statystyki są zawarte w strukturze rs_job_t).

Tworzenie delty

Tutaj musimy zacząć od odpowiedniego załadowania sygnatury do pamięci (bez tego będzie segfault), jak widać w pierwszej funkcji bufor wyjściowy nie jest używany.
C/C++
shared_ptr < rs_signature_t > loadSignatureIntoMemory( const string & signatureBuffer )
{
    string outputBuffer( signatureBuffer.size(), '\0' );
   
    rs_buffers_t buffers = fillBuffers( signatureBuffer, outputBuffer );
   
    rs_signature_t * sumsetRaw;
    shared_ptr < rs_job_t > job( rs_loadsig_begin( & sumsetRaw ), rs_job_free );
    shared_ptr < rs_signature_t > sumset( sumsetRaw, rs_free_sumset );
   
   
    rs_result result = rs_job_iter( job.get(), & buffers );
   
    if( buffers.avail_in )
    {
        throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
    }
   
    if( RS_BLOCKED == result )
    {
        throw runtime_error( "Data stream not ended!" );
    }
    else if( RS_DONE != result )
    {
        throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
    }
   
    result = rs_build_hash_table( sumset.get() );
    if( RS_DONE != result )
    {
        throw runtime_error( "Problem during creating hash table! " + string( rs_strerror( result ) ) );
    }
   
    rs_log_stats( rs_job_statistics( job.get() ) );
   
   
    return sumset;
}

string generateDeltaForData( const string & signatureBuffer, const string & dataNew )
{
    shared_ptr < rs_signature_t > sumset = loadSignatureIntoMemory( signatureBuffer );
   
    shared_ptr < rs_job_t > job( rs_delta_begin( sumset.get() ), rs_job_free );
   
    string outputBuffer( 2 * dataNew.size(), '\0' );
   
    rs_buffers_t buffers = fillBuffers( dataNew, outputBuffer );
   
    rs_result result;
    do
    {
        result = rs_job_iter( job.get(), & buffers );
       
        if( buffers.avail_in )
        {
            throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
        }
       
        if( RS_DONE == result )
        {
            break;
        }
        else if( RS_BLOCKED != result )
        {
            throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
        }
    }
    while( RS_BLOCKED == result );
   
    outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
   
    rs_log_stats( rs_job_statistics( job.get() ) );
   
   
    return outputBuffer;
}
W funkcji generującej deltę jak widać użyłem pętli, z nieznanych mi przyczyn bufor wyjściowy dla plików nie był ukończony (czyli wygenerowany plik delty w stosunku do pliku wygenerowanego przy pomocy funkcji operujących na całych plikach nie dodawał na końcu pliku jednego znaku: '\0', ciekawym jest, że inne funkcje, których używam w tym "rozdziale" nie wymagały takich zabiegów dla zapewnienia poprawności operacji, dla plików których używałem w testach).

Odtwarzanie nowych danych z danych w starszej wersji oraz delty

Tutaj niestety mamy pewien problem, gdyż mamy dwa źródła danych potrzebne równocześnie, funkcja rs_job_iter() przyjmuje tylko jeden buffor wejściowy, więc potrzebujemy podać drugi buffor do funkcji rs_patch_begin(), która ma następującą, dziwną formę:
C/C++
rs_job_t * rs_patch_begin( rs_copy_cb * copy_cb, void * copy_arg );

// where:
typedef rs_result rs_copy_cb( void * opaque, rs_long_t pos, size_t * len, void ** buf );
Niestety tylko jedna funkcja pasująca dla definicji rs_copy_cb jest dostarczona w bibliotece i znajduje się w pliku buf.c:
C/C++
rs_result rs_file_copy_cb( void * arg, rs_long_t pos, size_t * len, void ** buf );
implementacja ta operuje na deskryptorach plików języka C (FILE) ukrytych pod argumentem *arg, dlatego chcąc pominąć pliki języka C musimy napisać jej własną wersję.
Poniżej dwie przykładowe implementacje -dla strumieni i dla obiektu string:
C/C++
rs_result copyDataFromString( void * arg, rs_long_t pos, size_t * len, void ** buf )
{
    const string & data = * reinterpret_cast < string *>( arg );
   
    if( pos < 0 || pos >= data.size() )
    {
        cerr << "seek failed!\n";
        return RS_IO_ERROR;
    }
    if( 0 == * len )
    {
        cerr << "unexpected eof!\n";
        return RS_INPUT_ENDED;
    }
   
    memcpy( * buf, data.c_str() + pos, * len );
    return RS_DONE;
}

rs_result copyDataFromStream( void * arg, rs_long_t pos, size_t * len, void ** buf )
{
    istream & stream = * reinterpret_cast < istream *>( arg );
   
    if( !stream.seekg( pos, ios_base::beg ) )
    {
        cerr << "seek failed! " << endl;
        return RS_IO_ERROR;
    }
   
    if( !stream.read( static_cast < char *>( * buf ), * len ) )
    {
        cerr << "Error: " <<( stream.eof() ? "eofbit "
            : "" ) <<( stream.bad() ? "badbit "
            : "" ) <<( stream.fail() ? "failbit "
            : "" ) << endl;
        return RS_IO_ERROR;
    }
   
    return RS_DONE;
}

Mając odpowiednie implementacje czas na funkcję odtwarzającą dane w nowej wersji w oparciu o dane w starej wersji oraz deltę:
C/C++
string recreateNeverVersionOfData( const string & oldDataBuffer, const string & deltaBuffer )
{
    string outputBuffer( oldDataBuffer.size() + deltaBuffer.size(), '\0' );
   
    rs_buffers_t buffers = fillBuffers( deltaBuffer, outputBuffer );
   
    shared_ptr < rs_job_t > job( rs_patch_begin( copyDataFromString, const_cast < string *>( & oldDataBuffer ) ), rs_job_free );
   
    rs_result result = rs_job_iter( job.get(), & buffers );
   
    if( buffers.avail_in )
    {
        throw runtime_error( "! Not all buffer read! Necessairy greater output buffer!!!" );
    }
   
    if( RS_BLOCKED == result )
    {
        throw runtime_error( "Data stream not ended!" );
    }
    else if( RS_DONE != result )
    {
        throw runtime_error( "Error during stream processing: " + string( strerror( result ) ) );
    }
   
    outputBuffer.erase( buffers.next_out - outputBuffer.c_str() );
   
    rs_log_stats( rs_job_statistics( job.get() ) );
   
    return outputBuffer;
}
I tym oto sposobem przebrnęliśmy przez wszystkie funkcje do obsługi strumieni

Pull stream interface

Kolejnym sposobem używanie biblioteki są funkcje, które wywołują dostarczone przez programiste funkcje, a te z kolei pobierające większą ilość danych do bufora oraz wysyłają kolejne porcje przetworzonych danych do bufora wyjściowego, więcej informacji w dokumentacji, a przykłady użycia tych funkcji są w pliku whole.c. Podejrzewam, że niemalże wszyscy zainteresowani biblioteką zostaną nasyceni powyższymi przykładami, dlatego pozwolę sobie nie opisywać dodatkowo i tej części biblioteki.

Wielowątkowość

Na oficjalnej stronie poświęconej api librsync napisali, że biblioteka może być używana w programach wielowątkowych, chociaż nie synchronizuje ona wątków samodzielnie, każde zadanie tej biblioteki powinno być obsługiwane TYLKO przez pojedynczy wątek.

Minusy biblioteki

Nadszedł czas na smutną, aczkolwiek konieczną sekcja w opisie biblioteki. Mimo, iż opisana biblioteka zawiera świetne algorytmy, używane przez wiele bardzo popularnych programów, jest zaniedbana pod względem użytkowym (oczywiście mam nadzieję, że dzięki temu artykułowi będzie łatwiej się nią posługiwać). Dokumentacja jest bardzo słaba, niemalże w ogóle nie ma przykładów użycia, jest trudność w budowaniu na systemie Windows. Mimo tych trudności dostarczone algorytmy są świetne i dlatego warto jej użyć. Fajną informacją jest to iż ostatnimi czasu widać na githubie, że biblioteką ponownie ktoś się zajmuje.