snapraid/cmdline/search.c
Andrea Mazzoleni 4e6192b625
Some checks failed
Build / build-mac (push) Has been cancelled
Build / build-linux (push) Has been cancelled
Build / build-arm32 (push) Has been cancelled
Build / build-win (push) Has been cancelled
Build / build-freebsd (push) Has been cancelled
Build / build-musl (push) Has been cancelled
CodeQL Advanced / Analyze (c-cpp) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
Use 64-bit TommyDS hash functions on 64-bit architectures
2026-10-08 20:45:29 +02:00

365 lines
11 KiB
C

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2014 Andrea Mazzoleni
#include "os/portable.h"
#include "support.h"
#include "search.h"
#ifndef _WIN32
static const char* es(int err)
{
if (is_hw(err))
return "error_io";
else
return "error";
}
#endif
/****************************************************************************/
/* search */
static void search_file(struct snapraid_state* state, const char* path, data_off_t size, int64_t mtime_sec, int mtime_nsec)
{
struct snapraid_search_file* file;
tommy_hash_t file_hash;
file = malloc_nofail(sizeof(struct snapraid_search_file));
file->path = strdup_nofail(path);
file->size = size;
file->mtime_sec = mtime_sec;
file->mtime_nsec = mtime_nsec;
file_hash = file_stamp_hash(file->size, file->mtime_sec, file->mtime_nsec);
tommy_hashdyn_insert(&state->searchset, &file->node, file, file_hash);
}
void search_file_free(void* void_file)
{
struct snapraid_search_file* file = void_file;
free(file->path);
free(file);
}
struct search_file_compare_arg {
const struct snapraid_state* state;
const struct snapraid_block* block;
const struct snapraid_file* file;
unsigned char* buffer;
data_off_t offset;
size_t read_size;
int prevhash;
};
int search_file_compare(const void* void_arg, const void* void_data)
{
const struct search_file_compare_arg* arg = void_arg;
const struct snapraid_search_file* file = void_data;
const struct snapraid_state* state = arg->state;
unsigned char buffer_hash[HASH_MAX];
const char* path = file->path;
int f;
ssize_t ret;
size_t count;
/* compare file info */
if (arg->file->size != file->size)
return -1;
if (arg->file->mtime_sec != file->mtime_sec)
return -1;
if (arg->file->mtime_nsec != file->mtime_nsec)
return -1;
/* read the block and compare the hash */
f = open(path, O_RDONLY | O_BINARY);
if (f == -1) {
if (errno == ENOENT)
log_error(EUSER, "WARNING! Search candidate file '%s' disappeared.\n", path);
else
log_error(errno, "WARNING! Error opening search candidate '%s'. %s.\n", path, strerror(errno));
return -1;
}
count = 0;
do {
ret = pread(f, (char*)arg->buffer + count, arg->read_size - count, arg->offset + count);
if (ret < 0) {
if (errno == EINTR)
continue;
log_error(errno, "WARNING! Error reading search candidate '%s'. %s.\n", path, strerror(errno));
if (close(f) != 0)
log_error(errno, "WARNING! Error closing search candidate '%s'. %s.\n", path, strerror(errno));
return -1;
}
if (ret == 0) {
log_error(EUSER, "WARNING! Unexpected end of file in search candidate '%s'.\n", path);
if (close(f) != 0)
log_error(errno, "WARNING! Error closing search candidate '%s'. %s.\n", path, strerror(errno));
return -1;
}
count += ret;
} while (count < arg->read_size);
ret = close(f);
if (ret != 0) {
log_error(errno, "WARNING! Error closing search candidate '%s'. %s.\n", path, strerror(errno));
return -1;
}
if (arg->read_size != state->block_size) {
/* fill the remaining with 0 */
memset(arg->buffer + arg->read_size, 0, state->block_size - arg->read_size);
}
/* compute the block hash from the canonical buffer */
if (arg->prevhash)
memhash_block(state->prevhash, state->prevhashseed, buffer_hash, arg->buffer, arg->read_size, state->block_size);
else
memhash_block(state->hash, state->hashseed, buffer_hash, arg->buffer, arg->read_size, state->block_size);
/* check if the hash is matching */
if (memcmp(buffer_hash, arg->block->hash, BLOCK_HASH_SIZE) != 0)
return -1;
return 0;
}
int state_search_fetch(struct snapraid_state* state, int prevhash, struct snapraid_file* missing_file, block_off_t missing_file_pos, struct snapraid_block* missing_block, unsigned char* buffer)
{
struct snapraid_search_file* file;
tommy_hash_t file_hash;
struct search_file_compare_arg arg;
arg.state = state;
arg.block = missing_block;
arg.file = missing_file;
arg.buffer = buffer;
arg.offset = state->block_size * missing_file_pos;
arg.read_size = file_block_size(missing_file, missing_file_pos, state->block_size);
arg.prevhash = prevhash;
file_hash = file_stamp_hash(arg.file->size, arg.file->mtime_sec, arg.file->mtime_nsec);
/* search in the hashtable, and also check if the data matches the hash */
file = tommy_hashdyn_search(&state->searchset, search_file_compare, &arg, file_hash);
if (!file)
return -1;
/* if found, buffer is already set with data */
return 0;
}
static void search_dir(struct snapraid_state* state, struct snapraid_disk* disk, const struct fsidentity* root_identity, const char* dir, const char* sub)
{
DIR* d;
size_t mount_point_len = disk != 0 ? strlen(disk->mount_point) : 0;
size_t sub_len = strlen(sub);
d = opendir(dir);
if (!d) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error opening directory '%s'. %s.\n", dir, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
while (1) {
char path_next[PATH_MAX];
char sub_next[PATH_MAX];
char out[PATH_MAX];
struct snapraid_filter* reason = 0;
struct stat st;
const char* name;
struct dirent* dd;
/* clear errno to detect erroneous conditions */
errno = 0;
dd = readdir(d);
if (dd == 0 && errno != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error reading directory '%s'. %s.\n", dir, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
if (dd == 0) {
break; /* finished */
}
/* skip "." and ".." files */
name = dd->d_name;
if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0)))
continue;
pathprint(path_next, sizeof(path_next), "%s%s", dir, name);
pathprint(sub_next, sizeof(sub_next), "%s%s", sub, name);
/* exclude hidden files even before calling lstat() */
if (disk != 0 && filter_hidden(state->filter_hidden, dd) != 0) {
if (pathcmp(".snapraidignore", dd->d_name) != 0) {
msg_verbose("Excluding hidden '%s'\n", path_next);
continue;
}
}
/* exclude content files even before calling lstat() */
if (disk != 0 && filter_content(&state->contentlist, disk->mount_point, mount_point_len, sub, sub_len, name) != 0) {
msg_verbose("Excluding content '%s'\n", path_next);
continue;
}
/* exclude snapshot container even before calling lstat() */
if (disk != 0 && filter_snapshot(state->snapshot, sub, name) != 0) {
msg_verbose("Excluding snapshots directory '%s'\n", path_next);
continue;
}
#if HAVE_STRUCT_DIRENT_D_STAT
/* convert dirent to lstat result */
dirent_lstat(dd, &st);
/*
* If the st_mode field is missing, takes care to fill it using normal lstat()
* at now this can happen only in Windows (with HAVE_STRUCT_DIRENT_D_STAT defined),
* because we use a directory reading method that doesn't read info about ReparsePoint.
* Note that here we cannot call here lstat_sync(), because we don't know what kind
* of file is it, and lstat_sync() doesn't always work
*/
if (st.st_mode == 0) {
if (lstat(path_next, &st) != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error in stat file/directory '%s'. %s.\n", path_next, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
}
#else
/* get lstat info about the file */
if (lstat(path_next, &st) != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error in stat file/directory '%s'. %s.\n", path_next, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
#endif
if (S_ISREG(st.st_mode)) {
if (disk == 0
|| pathcmp(".snapraidignore", name) == 0
|| filter_path(&state->filterlist, &reason, disk->name, sub_next) == 0) {
search_file(state, path_next, st.st_size, st.st_mtime, STAT_NSEC(&st));
} else {
msg_verbose("Excluding file '%s' for rule '%s'\n", path_next, filter_type(reason, out, sizeof(out)));
}
} else if (S_ISDIR(st.st_mode)) {
if (disk == 0 || filter_subdir(&state->filterlist, &reason, disk->name, sub_next) == 0) {
#ifndef _WIN32
/*
* Automatic array search must stay within the same filesystem,
* mount, and subvolume boundaries as the normal data-disk scan.
* Explicit -i imports pass no root identity and may cross them.
*/
if (root_identity) {
struct fsidentity identity;
if (fsidentity(path_next, &st, &identity) != 0) {
/* LCOV_EXCL_START */
log_tag("%s:%u:%s:%s: Statx error. %s.\n", es(errno), 0, disk->name, esc_tag(path_next), strerror(errno));
log_fatal(errno, "Error accessing directory '%s'. %s.\n", path_next, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
if (identity.device != root_identity->device) {
log_tag("%s:%u:%s:%s: Ignoring mount point.\n", es(ESOFT), 0, disk->name, esc_tag(path_next));
log_error(ESOFT, "WARNING! Ignoring mount point '%s' because it appears to be in a different device\n", path_next);
continue;
} else if (identity.mnt_id != root_identity->mnt_id) {
log_tag("%s:%u:%s:%s: Ignoring mount point.\n", es(ESOFT), 0, disk->name, esc_tag(path_next));
log_error(ESOFT, "WARNING! Ignoring mount point '%s' because it belongs to a different mount\n", path_next);
continue;
} else if (identity.subvol != root_identity->subvol) {
log_tag("%s:%u:%s:%s: Ignoring nested subvolume.\n", es(ESOFT), 0, disk->name, esc_tag(path_next));
log_error(ESOFT, "WARNING! Ignoring nested subvolume '%s' because it belongs to a different subvolume\n", path_next);
continue;
}
}
#endif
pathslash(path_next, sizeof(path_next));
pathslash(sub_next, sizeof(sub_next));
search_dir(state, disk, root_identity, path_next, sub_next);
} else {
msg_verbose("Excluding directory '%s' for rule '%s'\n", path_next, filter_type(reason, out, sizeof(out)));
}
}
}
if (closedir(d) != 0) {
/* LCOV_EXCL_START */
log_fatal(errno, "Error closing directory '%s'. %s.\n", dir, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
}
void state_search(struct snapraid_state* state, const char* dir)
{
char path[PATH_MAX];
msg_progress("Importing...\n");
/* add the final slash */
pathimport(path, sizeof(path), dir);
pathslash(path, sizeof(path));
search_dir(state, 0, 0, path, "");
}
void state_search_array(struct snapraid_state* state)
{
tommy_node* i;
/* import from all the disks */
for (i = state->disklist; i != 0; i = i->next) {
struct snapraid_disk* disk = i->data;
/* skip data disks that are not accessible */
if (disk->skip_access)
continue;
msg_progress("Searching disk %s...\n", disk->name);
#ifndef _WIN32
struct stat st;
struct fsidentity identity;
if (lstat(disk->dir, &st) != 0) {
/* LCOV_EXCL_START */
log_tag("%s:%u:%s:%s: Stat error. %s.\n", es(errno), 0, disk->name, esc_tag(disk->dir), strerror(errno));
log_fatal(errno, "Error accessing directory '%s'. %s.\n", disk->dir, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
if (fsidentity(disk->dir, &st, &identity) != 0) {
/* LCOV_EXCL_START */
log_tag("%s:%u:%s:%s: Statx error. %s.\n", es(errno), 0, disk->name, esc_tag(disk->dir), strerror(errno));
log_fatal(errno, "Error accessing directory '%s'. %s.\n", disk->dir, strerror(errno));
exit(EXIT_FAILURE);
/* LCOV_EXCL_STOP */
}
search_dir(state, disk, &identity, disk->dir, "");
#else
search_dir(state, disk, 0, disk->dir, "");
#endif
}
}