]> git.ipfire.org Git - thirdparty/util-linux.git/commitdiff
include/fileeq: add functions to compare files content
authorKarel Zak <kzak@redhat.com>
Fri, 29 Oct 2021 11:37:36 +0000 (13:37 +0200)
committerKarel Zak <kzak@redhat.com>
Fri, 29 Oct 2021 11:37:36 +0000 (13:37 +0200)
Signed-off-by: Karel Zak <kzak@redhat.com>
include/all-io.h
include/fileeq.h [new file with mode: 0644]
lib/Makemodule.am
lib/fileeq.c [new file with mode: 0644]

index 5ed2d11a2e01033db30d14b33d7e979fd4ff66d6..e05e1e7ed7d000ec22dc0b141299eee6d5bd6c52 100644 (file)
@@ -111,5 +111,4 @@ static inline ssize_t sendfile_all(int out, int in, off_t *off, size_t count)
        return -1;
 #endif
 }
-
 #endif /* UTIL_LINUX_ALL_IO_H */
diff --git a/include/fileeq.h b/include/fileeq.h
new file mode 100644 (file)
index 0000000..28346c5
--- /dev/null
@@ -0,0 +1,53 @@
+#ifndef UTIL_LINUX_FILEEQ
+#define UTIL_LINUX_FILEEQ
+
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/* Number of bytes from the beginnning of the file we always
+ * compare by memcmp() */
+#define UL_FILEEQ_INTROSIZ     32
+
+struct ul_fileeq_data {
+       unsigned char intro[UL_FILEEQ_INTROSIZ];
+       unsigned char *blocks;
+       size_t nblocks;
+       size_t maxblocks;
+       int fd;
+       const char *name;
+       bool is_eof;
+};
+
+struct ul_fileeq {
+       int fd_api;     /* Crypto socket */
+       int fd_cip;     /* Cipher handler */
+
+       size_t readsiz;
+       uint64_t filesiz;
+       uint64_t blocksmax;
+       const struct ul_fileeq_method *method;
+
+       /* UL_FILEEQ_MEMCMP buffers */
+       unsigned char *buf_a;
+       unsigned char *buf_b;
+       unsigned char *buf_last;
+};
+
+extern int ul_fileeq_init(struct ul_fileeq *eq, const char *method);
+extern void ul_fileeq_deinit(struct ul_fileeq *eq);
+
+
+extern int ul_fileeq_data_associated(struct ul_fileeq_data *data);
+extern void ul_fileeq_data_close_file(struct ul_fileeq_data *data);
+extern void ul_fileeq_data_init(struct ul_fileeq_data *data);
+extern void ul_fileeq_data_deinit(struct ul_fileeq_data *data);
+extern void ul_fileeq_data_set_file(struct ul_fileeq_data *data,
+                                   const char *name);
+extern size_t ul_fileeq_set_size(struct ul_fileeq *eq, uint64_t filesiz,
+                                 size_t readsiz, size_t memsiz);
+
+extern int ul_fileeq(struct ul_fileeq *eq,
+              struct ul_fileeq_data *a, struct ul_fileeq_data *b);
+
+#endif /* UTIL_LINUX_FILEEQ */
index d32a20a2c40fb9c42e569cfe01424de03b2b21f1..327350d1d3d73911e72eee4a65a65001382757bf 100644 (file)
@@ -84,6 +84,7 @@ check_PROGRAMS += \
        test_buffer \
        test_canonicalize \
        test_colors \
+       test_fileeq \
        test_fileutils \
        test_ismounted \
        test_pwdutils \
@@ -188,6 +189,9 @@ test_linux_version_SOURCES = lib/linux_version.c
 test_linux_version_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM_LINUXVERSION
 endif
 
+test_fileeq_SOURCES = lib/fileeq.c
+test_fileeq_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM_FILEEQ
+
 test_fileutils_SOURCES = lib/fileutils.c
 test_fileutils_CFLAGS = $(AM_CFLAGS) -DTEST_PROGRAM_FILEUTILS
 
diff --git a/lib/fileeq.c b/lib/fileeq.c
new file mode 100644 (file)
index 0000000..fdcdb65
--- /dev/null
@@ -0,0 +1,638 @@
+/*
+ * compare files content
+ *
+ * The goal is to minimize number of data we need to read from the files and be
+ * ready to compare large set of files, it means reuse the previous data if
+ * possible. It never read entire file if not necessary.
+ *
+ * The another goal is to minimize number of open files (imagine "hardlink /"),
+ * the code can open only two files and reopen the file next time if
+ * necessary.
+ *
+ * This code supports multiple comparison methods. The very basic step which is
+ * generic for all methods is to read and compare an "intro" (a few bytes from
+ * the begging of the file). This intro buffer is always cached in 'struct
+ * ul_fileeq_data', this intro buffer is addresses as block=0. This primitive
+ * thing can reduce a lot ...
+ *
+ * The next steps depend on selected method:
+ *
+ *  * memcmp method: always read data to userspace, nothing is cached, compare
+ *  directly files content; fast for small sets of the small files.
+ *
+ *  * Linux crypto API: zero-copy method based on sendfile(), data blocks are
+ *  send to the kernel hash functions (sha1, ...), and only hash digest is read
+ *  and cached in usersapce. Fast for large set of (large) files.
+ *
+ *
+ * No copyright is claimed.  This code is in the public domain; do with
+ * it what you wish.
+ *
+ * Written by Karel Zak <kzak@redhat.com> [October 2021]
+ */
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+/* Linux crypto */
+#ifdef HAVE_LINUX_IF_ALG_H
+# include <sys/socket.h>
+# include <linux/if_alg.h>
+# include <sys/param.h>
+# include <sys/sendfile.h>
+# define USE_HARDLINK_CRYPTOAPI        1
+#endif
+
+#include "c.h"
+#include "all-io.h"
+#include "fileeq.h"
+#include "debug.h"
+
+static UL_DEBUG_DEFINE_MASK(ulfileeq);
+UL_DEBUG_DEFINE_MASKNAMES(ulfileeq) = UL_DEBUG_EMPTY_MASKNAMES;
+
+#define ULFILEEQ_DEBUG_INIT    (1 << 1)
+#define ULFILEEQ_DEBUG_CRYPTO  (1 << 2)
+#define ULFILEEQ_DEBUG_DATA    (1 << 3)
+#define ULFILEEQ_DEBUG_EQ      (1 << 4)
+
+#define DBG(m, x)       __UL_DBG(ulfileeq, ULFILEEQ_DEBUG_, m, x)
+#define ON_DBG(m, x)    __UL_DBG_CALL(ulfileeq, ULFILEEQ_DEBUG_, m, x)
+
+#define UL_DEBUG_CURRENT_MASK  UL_DEBUG_MASK(ulfileeq)
+#include "debugobj.h"
+
+static void ul_fileeq_init_debug(void)
+{
+       if (ulfileeq_debug_mask)
+               return;
+       __UL_INIT_DEBUG_FROM_ENV(ulfileeq, ULFILEEQ_DEBUG_, 0, ULFILEEQ_DEBUG);
+}
+
+enum {
+       UL_FILEEQ_MEMCMP,
+       UL_FILEEQ_SHA1,
+       UL_FILEEQ_SHA256,
+       UL_FILEEQ_CRC32
+};
+
+struct ul_fileeq_method {
+       int id;
+       const char *name;       /* name used by applications */
+       const char *kname;      /* name used by kernel crypto */
+       short digsiz;
+};
+
+static const struct ul_fileeq_method ul_eq_methods[] = {
+       [UL_FILEEQ_MEMCMP] = {
+               .id = UL_FILEEQ_MEMCMP, .name = "memcmp"
+       },
+#ifdef USE_HARDLINK_CRYPTOAPI
+       [UL_FILEEQ_SHA1] = {
+               .id = UL_FILEEQ_SHA1, .name = "sha1",
+               .digsiz = 20, .kname = "sha1"
+       },
+       [UL_FILEEQ_SHA256] = {
+               .id = UL_FILEEQ_SHA256, .name = "sha256",
+               .digsiz = 32, .kname = "sha256"
+       },
+
+       [UL_FILEEQ_CRC32] = {
+               .id = UL_FILEEQ_CRC32, .name = "crc32",
+               .digsiz = 4, .kname = "crc32c"
+       }
+#endif
+};
+
+#ifdef USE_HARDLINK_CRYPTOAPI
+static void deinit_crypto_api(struct ul_fileeq *eq)
+{
+       if (!eq)
+               return;
+
+       DBG(CRYPTO, ul_debugobj(eq, "deinit"));
+
+       if (eq->fd_cip >= 0)
+               close(eq->fd_cip);
+       if (eq->fd_api >= 0)
+               close(eq->fd_api);
+
+       eq->fd_cip = eq->fd_api = -1;
+
+}
+
+static int init_crypto_api(struct ul_fileeq *eq)
+{
+       struct sockaddr_alg sa = {
+               .salg_family = AF_ALG,
+               .salg_type   = "hash",
+       };
+
+       assert(eq->method);
+       assert(eq->method->kname);
+       assert(eq->fd_api == -1);
+       assert(eq->fd_cip == -1);
+
+       DBG(CRYPTO, ul_debugobj(eq, "init [%s]", eq->method->kname));
+
+       assert(sizeof(sa.salg_name) > strlen(eq->method->kname) + 1);
+       memcpy(&sa.salg_name, eq->method->kname, strlen(eq->method->kname) + 1);
+
+       if ((eq->fd_api = socket(AF_ALG, SOCK_SEQPACKET, 0)) < 0)
+               goto fail;
+       if (bind(eq->fd_api, (struct sockaddr *) &sa, sizeof(sa)) != 0)
+               goto fail;
+       if ((eq->fd_cip = accept(eq->fd_api, NULL, 0)) < 0)
+               goto fail;
+       return 0;
+fail:
+       deinit_crypto_api(eq);
+       return -1;
+}
+#endif
+
+int ul_fileeq_init(struct ul_fileeq *eq, const char *method)
+{
+       size_t i;
+
+       ul_fileeq_init_debug();
+       DBG(EQ, ul_debugobj(eq, "init [%s]", method));
+
+       memset(eq, 0, sizeof(*eq));
+       eq->fd_api = -1;
+       eq->fd_cip = -1;
+
+       for (i = 0; i < ARRAY_SIZE(ul_eq_methods); i++) {
+               const struct ul_fileeq_method *m = &ul_eq_methods[i];
+
+               if (strcmp(m->name, method) == 0) {
+                       eq->method = m;
+                       break;
+               }
+       }
+
+       if (!eq->method)
+               return -1;
+#ifdef USE_HARDLINK_CRYPTOAPI
+       if (eq->method->id != UL_FILEEQ_MEMCMP
+           && init_crypto_api(eq) != 0)
+               return -1;
+#endif
+       return 0;
+}
+
+void ul_fileeq_deinit(struct ul_fileeq *eq)
+{
+       if (!eq)
+               return;
+
+       DBG(EQ, ul_debugobj(eq, "deinit"));
+#ifdef USE_HARDLINK_CRYPTOAPI
+       deinit_crypto_api(eq);
+#endif
+       free(eq->buf_a);
+       free(eq->buf_b);
+}
+
+void ul_fileeq_data_close_file(struct ul_fileeq_data *data)
+{
+       assert(data);
+
+       if (data->fd >= 0) {
+               DBG(DATA, ul_debugobj(data, "close"));
+               close(data->fd);
+       }
+       data->fd = -1;
+}
+
+void ul_fileeq_data_init(struct ul_fileeq_data *data)
+{
+       DBG(DATA, ul_debugobj(data, "init"));
+       memset(data, 0, sizeof(*data));
+       data->fd = -1;
+}
+
+void ul_fileeq_data_deinit(struct ul_fileeq_data *data)
+{
+       assert(data);
+
+       DBG(DATA, ul_debugobj(data, "deinit"));
+       free(data->blocks);
+       data->blocks = NULL;
+       data->nblocks = 0;
+       data->maxblocks = 0;
+       data->is_eof = 0;
+       data->name = NULL;
+
+       ul_fileeq_data_close_file(data);
+}
+
+int ul_fileeq_data_associated(struct ul_fileeq_data *data)
+{
+       return data->name != NULL;
+}
+
+void ul_fileeq_data_set_file(struct ul_fileeq_data *data, const char *name)
+{
+       assert(data);
+       assert(name);
+
+       DBG(DATA, ul_debugobj(data, "set file: %s", name));
+       ul_fileeq_data_init(data);
+       data->name = name;
+}
+
+size_t ul_fileeq_set_size(struct ul_fileeq *eq, uint64_t filesiz,
+                        size_t readsiz, size_t memsiz)
+{
+       uint64_t nreads, maxdigs;
+       size_t digsiz;
+
+       assert(eq);
+
+       eq->filesiz = filesiz;
+
+       switch (eq->method->id) {
+       case UL_FILEEQ_MEMCMP:
+               /* align file size */
+               filesiz = (filesiz + readsiz) / readsiz * readsiz;
+               break;
+       default:
+               digsiz = eq->method->digsiz;
+               if (readsiz < digsiz)
+                       readsiz = digsiz;
+               /* align file size */
+               filesiz = (filesiz + readsiz) / readsiz * readsiz;
+               /* calculate limits */
+               maxdigs = memsiz / digsiz;
+               nreads = filesiz / readsiz;
+               /* enlarge readsize for large files */
+               if (nreads > maxdigs)
+                       readsiz = filesiz / maxdigs;
+               break;
+       }
+
+       eq->readsiz = readsiz;
+       eq->blocksmax = filesiz / readsiz;
+
+       DBG(EQ, ul_debugobj(eq, "set sizes: filesiz=%ju, maxblocks=%zu, readsiz=%zu",
+                               eq->filesiz, eq->blocksmax, eq->readsiz));
+       return eq->blocksmax;
+}
+
+static unsigned char *get_buffer(struct ul_fileeq *eq)
+{
+       if (!eq->buf_a)
+               eq->buf_a = malloc(eq->readsiz);
+       if (!eq->buf_b)
+               eq->buf_b = malloc(eq->readsiz);
+
+       if (!eq->buf_a || !eq->buf_b)
+               return NULL;
+
+       if (eq->buf_last == eq->buf_b)
+               eq->buf_last = eq->buf_a;
+       else
+               eq->buf_last = eq->buf_b;
+
+       return eq->buf_last;
+}
+
+#define get_cached_nblocks(_d) \
+                       ((_d)->nblocks ? (_d)->nblocks - 1 : 0)
+
+#define get_cached_offset(_e, _d) \
+                       ((_d)->nblocks == 0 ? 0 : \
+                               sizeof((_d)->intro) \
+                               + (get_cached_nblocks(_d) * (_e)->readsiz))
+
+
+static int get_fd(struct ul_fileeq *eq, struct ul_fileeq_data *data, off_t *off)
+{
+       off_t o = get_cached_offset(eq, data);
+
+       assert(eq);
+       assert(data);
+
+
+       if (data->fd < 0) {
+               DBG(DATA, ul_debugobj(data, "open: %s", data->name));
+               data->fd = open(data->name, O_RDONLY);
+               if (data->fd < 0)
+                       return data->fd;
+
+#if defined(POSIX_FADV_SEQUENTIAL) && defined(HAVE_POSIX_FADVISE)
+               ignore_result( posix_fadvise(data->fd, o, 0, POSIX_FADV_SEQUENTIAL) );
+#endif
+               if (o) {
+                       DBG(DATA, ul_debugobj(data, "lseek off=%ju", (uintmax_t) o));
+                       lseek(data->fd, o, SEEK_SET);
+               }
+       }
+
+       if (off)
+               *off = o;
+
+       return data->fd;
+}
+
+static void memcmp_reset(struct ul_fileeq *eq, struct ul_fileeq_data *data)
+{
+       /* only intro[] is cached */
+       if (data->nblocks)
+               data->nblocks = 1;
+       /* reset file possition */
+       if (data->fd >= 0)
+               lseek(data->fd, get_cached_offset(eq, data), SEEK_SET);
+       data->is_eof = 0;
+}
+
+static ssize_t read_block(struct ul_fileeq *eq, struct ul_fileeq_data *data,
+                               size_t n, unsigned char **block)
+{
+       int fd;
+       off_t off = 0;
+       ssize_t rsz;
+
+       if (data->is_eof || n > eq->blocksmax)
+               return 0;
+
+       fd = get_fd(eq, data, &off);
+       if (fd < 0)
+               return fd;
+
+       DBG(DATA, ul_debugobj(data, " read block off=%ju", (uintmax_t) off));
+
+       *block = get_buffer(eq);
+       if (!*block)
+               return -ENOMEM;
+
+       rsz = read_all(data->fd, (char *) *block, eq->readsiz);
+       if (rsz < 0) {
+               DBG(DATA, ul_debugobj(data, "  read failed"));
+               return rsz;
+       }
+       off += rsz;
+       data->nblocks++;
+
+       if (rsz == 0 || (uint64_t) off >= eq->filesiz) {
+               data->is_eof = 1;
+               ul_fileeq_data_close_file(data);
+       }
+
+       DBG(DATA, ul_debugobj(data, "  read sz=%zu", rsz));
+       return rsz;
+}
+
+#ifdef USE_HARDLINK_CRYPTOAPI
+static ssize_t get_digest(struct ul_fileeq *eq, struct ul_fileeq_data *data,
+                               size_t n, unsigned char **block)
+{
+       off_t off = 0;
+       ssize_t rsz;
+       size_t sz;
+       int fd;
+
+       if (n > eq->blocksmax)
+               return 0;
+
+       /* return already cached if alvalable */
+       if (n < get_cached_nblocks(data)) {
+               DBG(DATA, ul_debugobj(data, " digest cached"));
+               assert(data->blocks);
+               *block = data->blocks + (n * eq->method->digsiz);
+               return eq->method->digsiz;
+       }
+
+       if (data->is_eof) {
+               DBG(DATA, ul_debugobj(data, " file EOF"));
+               return 0;
+       }
+
+       /* read new block */
+       fd = get_fd(eq, data, &off);
+       if (fd < 0)
+               return fd;
+
+       DBG(DATA, ul_debugobj(data, " read digest off=%ju", (uintmax_t) off));
+
+       sz = eq->method->digsiz;
+
+       if (!data->blocks) {
+               DBG(DATA, ul_debugobj(data, "  alloc cache %zu", eq->blocksmax * sz));
+               data->blocks = malloc(eq->blocksmax * sz);
+               if (!data->blocks)
+                       return -ENOMEM;
+       }
+
+       assert(n <= eq->blocksmax);
+
+       rsz = sendfile(eq->fd_cip, data->fd, NULL, eq->readsiz);
+       DBG(DATA, ul_debugobj(data, "  sent %zu [%zu wanted] to cipher", rsz, eq->readsiz));
+
+       if (rsz < 0)
+               return rsz;
+
+       off += rsz;
+
+       /* get block digest (note 1st block is data->intro */
+       *block = data->blocks + (n * eq->method->digsiz);
+       rsz = read_all(eq->fd_cip, (char *) *block, sz);
+
+       if (rsz > 0)
+               data->nblocks++;
+       if (rsz == 0 || (uint64_t) off >= eq->filesiz) {
+               data->is_eof = 1;
+               ul_fileeq_data_close_file(data);
+       }
+       DBG(DATA, ul_debugobj(data, "  get %zuB digest", rsz));
+       return rsz;
+}
+#endif
+
+static ssize_t get_intro(struct ul_fileeq *eq, struct ul_fileeq_data *data,
+                               unsigned char **block)
+{
+       if (data->nblocks == 0) {
+               int fd = get_fd(eq, data, NULL);
+               ssize_t rsz;
+
+               if (fd < 0)
+                       return -1;
+               rsz = read_all(fd, (char *) data->intro, sizeof(data->intro));
+               DBG(DATA, ul_debugobj(data, " read %zu bytes intro", sizeof(data->intro)));
+               if (rsz <= 0)
+                       return -1;
+               data->nblocks = 1;
+       }
+
+       DBG(DATA, ul_debugobj(data, " return intro"));
+       *block = data->intro;
+       return sizeof(data->intro);
+}
+
+static ssize_t get_cmp_data(struct ul_fileeq *eq, struct ul_fileeq_data *data,
+                               size_t blockno, unsigned char **block)
+{
+       if (blockno == 0)
+               return get_intro(eq, data, block);
+
+       blockno--;
+
+       switch (eq->method->id) {
+       case UL_FILEEQ_MEMCMP:
+               return read_block(eq, data, blockno, block);
+       default:
+               break;
+       }
+#ifdef USE_HARDLINK_CRYPTOAPI
+       return get_digest(eq, data, blockno, block);
+#else
+       return -1;
+#endif
+}
+
+#define CMP(a, b) ((a) > (b) ? 1 : ((a) < (b) ? -1 : 0))
+
+int ul_fileeq(struct ul_fileeq *eq,
+             struct ul_fileeq_data *a, struct ul_fileeq_data *b)
+{
+       int cmp;
+       size_t n = 0;
+
+       DBG(EQ, ul_debugobj(eq, "--> compare %s %s", a->name, b->name));
+
+       if (eq->method->id == UL_FILEEQ_MEMCMP) {
+               memcmp_reset(eq, a);
+               memcmp_reset(eq, b);
+       }
+
+       do {
+               unsigned char *da, *db;
+               ssize_t ca, cb;
+
+               DBG(EQ, ul_debugobj(eq, "compare block #%zu", n));
+
+               ca = get_cmp_data(eq, a, n, &da);
+               if (ca < 0)
+                       goto done;
+               cb = get_cmp_data(eq, b, n, &db);
+               if (cb < 0)
+                       goto done;
+               if (ca != cb || ca == 0) {
+                       cmp = CMP(ca, cb);
+                       break;
+
+               }
+               cmp = memcmp(da, db, ca);
+               DBG(EQ, ul_debugobj(eq, "#%zu=%s", n, cmp == 0 ? "match" : "not-match"));
+               n++;
+       } while (cmp == 0);
+
+       if (cmp == 0) {
+               if (!a->is_eof || !b->is_eof)
+                       goto done; /* filesize chnaged? */
+
+               DBG(EQ, ul_debugobj(eq, "<-- MATCH"));
+               return 1;
+       }
+done:
+       DBG(EQ, ul_debugobj(eq, " <-- NOT-MATCH"));
+       return 0;
+}
+
+#ifdef TEST_PROGRAM_FILEEQ
+# include <getopt.h>
+# include <err.h>
+
+int main(int argc, char *argv[])
+{
+       struct ul_fileeq eq;
+       struct ul_fileeq_data a, b, c;
+       const char *method = "sha1";
+       static const struct option longopts[] = {
+               { "method", required_argument, NULL, 'm' },
+               { "help", no_argument, NULL, 'h' },
+               { NULL, 0, NULL, 0 }
+       };
+       int ch, rc;
+       const char *file_a = NULL, *file_b = NULL, *file_c = NULL;
+       struct stat st_a, st_b, st_c;
+
+       while ((ch = getopt_long(argc, argv, "m:", longopts, NULL)) != -1) {
+               switch (ch) {
+               case 'm':
+                       method = optarg;
+                       break;
+               case 'h':
+                       printf("usage: %s [options] <file> <file>\n"
+                               " -m, --method <memcmp|sha1|crc32>    compare method\n",
+                               program_invocation_short_name);
+                       return EXIT_FAILURE;
+               }
+       }
+
+       if (optind < argc)
+               file_a = argv[optind++];
+       if (optind < argc)
+               file_b = argv[optind++];
+       if (optind < argc)
+               file_c = argv[optind++];
+
+       if (!file_a || !file_b)
+               errx(EXIT_FAILURE, "no files specified, see --help");
+
+       if (stat(file_a, &st_a) != 0 || !S_ISREG(st_a.st_mode))
+               errx(EXIT_FAILURE, "%s: wrong file", file_a);
+       if (stat(file_b, &st_b) != 0 || !S_ISREG(st_b.st_mode))
+               errx(EXIT_FAILURE, "%s: wrong file", file_b);
+       if (file_c && (stat(file_c, &st_c) != 0 || !S_ISREG(st_c.st_mode)))
+               errx(EXIT_FAILURE, "%s: wrong file", file_c);
+
+
+       if (st_a.st_size != st_b.st_size ||
+           (file_c && st_a.st_size != st_c.st_size))
+               errx(EXIT_FAILURE, "size of the files does not match");
+
+
+       rc = ul_fileeq_init(&eq, method);
+       if (rc != 0 && strcmp(method, "memcmp") != 0) {
+               method = "memcmp";
+               rc = ul_fileeq_init(&eq, method);
+       }
+       if (rc < 0)
+               err(EXIT_FAILURE, "failed to initialize files comparior");
+
+       ul_fileeq_data_set_file(&a, file_a);
+       ul_fileeq_data_set_file(&b, file_b);
+
+       /* 3rd is optional */
+       if (file_c)
+               ul_fileeq_data_set_file(&c, file_c);
+
+       /*                     filesiz,      readsiz,   memsiz */
+       ul_fileeq_set_size(&eq, st_a.st_size, 1024*1024, 4*1024);
+
+       rc = ul_fileeq(&eq, &a, &b);
+
+       printf("1st vs. 2nd: %s\n", rc == 1 ? "MATCH" : "NOT-MATCH");
+       if (file_c) {
+               rc = ul_fileeq(&eq, &a, &c);
+               printf("1st vs. 3rd: %s\n", rc == 1 ? "MATCH" : "NOT-MATCH");
+
+               rc = ul_fileeq(&eq, &b, &c);
+               printf("2st vs. 3rd: %s\n", rc == 1 ? "MATCH" : "NOT-MATCH");
+       }
+
+       ul_fileeq_data_deinit(&a);
+       ul_fileeq_data_deinit(&b);
+
+       if (file_c)
+               ul_fileeq_data_deinit(&c);
+
+       ul_fileeq_deinit(&eq);
+       return EXIT_FAILURE;
+}
+#endif /* TEST_PROGRAM_FILEEQ */