Release v2.26.0 #284

Merged
TapTap merged 210 commits from dev into main 2026-09-18 19:05:52 +02:00
4 changed files with 317 additions and 15 deletions
Showing only changes of commit 583d3c8edb - Show all commits
+101
View File
@@ -136,6 +136,7 @@ typedef struct {
ScannerOptions options; ScannerOptions options;
FilterRuleList* base_filters; /* owned; may be NULL */ FilterRuleList* base_filters; /* owned; may be NULL */
HardLinkTable* hardlinks; /* owned; may be NULL */ HardLinkTable* hardlinks; /* owned; may be NULL */
char* relative_prefix; /* owned -R prefix; may be NULL */
} PreparedScanner; } PreparedScanner;
/* Build the scanner options for one scan. Returns false and logs on failure. */ /* Build the scanner options for one scan. Returns false and logs on failure. */
@@ -144,6 +145,7 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
return false; return false;
out->base_filters = NULL; out->base_filters = NULL;
out->hardlinks = NULL; out->hardlinks = NULL;
out->relative_prefix = NULL;
memset(&out->options, 0, sizeof(out->options)); memset(&out->options, 0, sizeof(out->options));
int rule_count = config->filters ? config->filters->size : 0; int rule_count = config->filters ? config->filters->size : 0;
@@ -206,6 +208,19 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
options->per_dir_filters = config->per_dir_filter; options->per_dir_filters = config->per_dir_filter;
options->dirs = config->dirs; options->dirs = config->dirs;
options->relative = config->relative; options->relative = config->relative;
/* -R/--relative outside --files-from reconstructs every destination path from
* the source spec (rsync's '/./' cut point). With --files-from the listed
* entry already supplies the bare relative path, so no prefix is built. */
if (config->relative && config->files_from_set == NULL && config->send_directory) {
out->relative_prefix = scanner_relative_prefix(config->send_directory);
if (!out->relative_prefix) {
log_message(LOG_LEVEL_ERROR, "memory allocation failed building --relative path prefix");
filter_rule_list_free(out->base_filters);
out->base_filters = NULL;
return false;
}
options->relative_prefix = out->relative_prefix;
}
options->prune_empty_dirs = config->prune_empty_dirs; options->prune_empty_dirs = config->prune_empty_dirs;
options->ignore_io_errors = config->ignore_errors; options->ignore_io_errors = config->ignore_errors;
options->ignore_missing_args = config->ignore_missing_args || config->delete_missing_args; options->ignore_missing_args = config->ignore_missing_args || config->delete_missing_args;
@@ -239,6 +254,85 @@ static void prepared_scanner_destroy(PreparedScanner* prepared) {
prepared->base_filters = NULL; prepared->base_filters = NULL;
hardlink_table_destroy(prepared->hardlinks); hardlink_table_destroy(prepared->hardlinks);
prepared->hardlinks = NULL; prepared->hardlinks = NULL;
free(prepared->relative_prefix);
prepared->relative_prefix = NULL;
}
/* -R/--relative implied directories: rsync transmits the metadata of the
* parent directories implied by the source path (every prefix component above
* the source root) so the receiver applies their attributes to the created
* parents. FastSync's scan only covers the source root and below, so append
* one metadata-only directory entry per implied ancestor. --no-implied-dirs
* suppresses this exactly like rsync. A missing ancestor is never fatal. */
static bool append_implied_dir_times(const Config* config, ArrayList* dir_entries) {
if (!dir_entries || !config->relative || config->files_from_set != NULL ||
config->no_implied_dirs || !config->send_directory)
return true;
char* prefix = scanner_relative_prefix(config->send_directory);
if (!prefix)
return true;
int ncomp = 0;
for (const char* s = prefix; *s;) {
while (*s == '/')
s++;
if (!*s)
break;
while (*s && *s != '/')
s++;
ncomp++;
}
if (ncomp <= 1) {
free(prefix);
return true;
}
char* fs = str_dup(config->send_directory);
if (!fs) {
free(prefix);
return true;
}
size_t flen = strlen(fs);
while (flen > 1 && fs[flen - 1] == '/')
fs[--flen] = '\0';
bool ok = true;
/* Walk the source path upwards one component at a time; the previous
iteration's truncation is restored so every ancestor is stat'ed in full. */
for (int depth = ncomp - 2; depth >= 0 && ok; depth--) {
char* slash = strrchr(fs, '/');
if (!slash || slash == fs)
break;
*slash = '\0';
char* p = prefix;
int c = 0;
while (c <= depth) {
while (*p == '/')
p++;
while (*p && *p != '/')
p++;
c++;
}
char saved = *p;
*p = '\0';
struct stat st;
if (stat(fs, &st) == 0 && S_ISDIR(st.st_mode)) {
File* file = file_create(fs);
if (!file) {
ok = false;
} else {
file->is_dir = true;
file->metadata =
file_metadata_create(fs, &st, config->preserve_atimes, config->preserve_crtimes);
file->send_path = str_dup(prefix);
if (!file->metadata || !file->send_path || !array_list_add(dir_entries, file)) {
file_destroy(file);
ok = false;
}
}
}
*p = saved;
}
free(fs);
free(prefix);
return ok;
} }
/* True when some --files-from entry is an ancestor-or-equal directory of /* True when some --files-from entry is an ancestor-or-equal directory of
@@ -2098,6 +2192,11 @@ static int scan_directory_multithreaded(void* pipeline_context) {
parallel workers append under the context's dedicated mutex. */ parallel workers append under the context's dedicated mutex. */
prepared.options.dir_entries = context->dir_entries; prepared.options.dir_entries = context->dir_entries;
prepared.options.dir_entries_mutex = &context->dir_entries_mutex; prepared.options.dir_entries_mutex = &context->dir_entries_mutex;
if (!append_implied_dir_times(context->config, context->dir_entries)) {
pipeline_cancel(context);
protocol_session_unbind();
return thrd_error;
}
/* The keep-set manifest for the late modes is built from this data pass, so /* The keep-set manifest for the late modes is built from this data pass, so
the parallel scanner records the protected excluded prefixes and the the parallel scanner records the protected excluded prefixes and the
synchronized directories here (the size-prune protection is collected in synchronized directories here (the size-prune protection is collected in
@@ -2434,6 +2533,8 @@ int send_files(Config* config) {
dir_entries = array_list_create(file_destroy); dir_entries = array_list_create(file_destroy);
if (!dir_entries) if (!dir_entries)
goto send_fail; goto send_fail;
if (!append_implied_dir_times(config, dir_entries))
goto send_fail;
} }
if (config->remove_source_files) if (config->remove_source_files)
remove_sources = array_list_create(source_file_destroy); remove_sources = array_list_create(source_file_destroy);
+149 -14
View File
@@ -220,6 +220,44 @@ char* scanner_path_relative(const char* root, const char* fs_path) {
return str_dup(fs_path + root_len + 1); return str_dup(fs_path + root_len + 1);
} }
/* -R/--relative destination-relative prefix reconstructed from a source spec:
* everything after the first '.' path component (rsync's '/./' cut point),
* with leading/trailing slashes removed; or the whole spec (normalized) when
* there is no cut. Returns "" for the receive root. Exposed for tests. */
char* scanner_relative_prefix(const char* spec) {
if (!spec || spec[0] == '\0')
return NULL;
const char* after = spec;
if (spec[0] == '.' && spec[1] == '/') {
after = spec + 2;
} else {
const char* cut = strstr(spec, "/./");
if (cut)
after = cut + 3;
}
size_t cap = strlen(spec) + 1;
char* out = malloc(cap);
if (!out)
return NULL;
size_t len = 0;
for (const char* s = after; *s;) {
while (*s == '/')
s++;
const char* comp = s;
while (*s && *s != '/')
s++;
size_t clen = (size_t)(s - comp);
if (clen == 0 || (clen == 1 && comp[0] == '.'))
continue;
if (len)
out[len++] = '/';
memcpy(out + len, comp, clen);
len += clen;
}
out[len] = '\0';
return out;
}
/* Relative path of a child entry below the current directory. */ /* Relative path of a child entry below the current directory. */
static char* child_rel_path(const char* parent_rel, const char* name) { static char* child_rel_path(const char* parent_rel, const char* name) {
if (!parent_rel || parent_rel[0] == '\0') if (!parent_rel || parent_rel[0] == '\0')
@@ -227,6 +265,15 @@ static char* child_rel_path(const char* parent_rel, const char* name) {
return path_cat(parent_rel, name); return path_cat(parent_rel, name);
} }
/* Destination-relative wire path for an entry under an -R prefix. */
static char* scanner_prefix_send_path(const char* prefix, const char* rel) {
if (prefix[0] == '\0')
return str_dup(rel);
if (rel[0] == '\0')
return str_dup(prefix);
return path_cat(prefix, rel);
}
/* Apply the --files-from allow-set and the filter layer to one entry. */ /* Apply the --files-from allow-set and the filter layer to one entry. */
static bool entry_passes_selection(const FileListSet* file_list, const FilterRuleList* base, static bool entry_passes_selection(const FileListSet* file_list, const FilterRuleList* base,
const FilterNode* node, const char* rel, const char* leaf, const FilterNode* node, const char* rel, const char* leaf,
@@ -367,12 +414,25 @@ static bool scanner_record_synced_dir(const ScannerOptions* options, const char*
return true; return true;
if (!file_list_dir_in_scope(options->file_list, rel)) if (!file_list_dir_in_scope(options->file_list, rel))
return true; return true;
const char* dest = relative_mode ? rel : fs_path; char* prefixed = NULL;
const char* dest;
if (relative_mode) {
dest = rel;
} else if (options->relative_prefix) {
prefixed = scanner_prefix_send_path(options->relative_prefix, rel);
if (!prefixed)
return false;
dest = prefixed;
} else {
dest = fs_path;
}
if (dest[0] == '/') if (dest[0] == '/')
dest++; dest++;
if (dest[0] == '\0') if (dest[0] == '\0')
dest = "."; dest = ".";
return excluded_sink_append(options->synced_dirs, options->excluded_mutex, dest); bool ok = excluded_sink_append(options->synced_dirs, options->excluded_mutex, dest);
free(prefixed);
return ok;
} }
/* Merge the open directory's own .rsync-filter rules into the inherited /* Merge the open directory's own .rsync-filter rules into the inherited
@@ -672,7 +732,8 @@ static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
* non-directory path is silently skipped (the transfer is unaffected); an * non-directory path is silently skipped (the transfer is unaffected); an
* allocation failure is fatal and reported to the caller. */ * allocation failure is fatal and reported to the caller. */
static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const char* root_path, static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const char* root_path,
const char* fs_path, bool relative_mode, bool preserve_atimes, const char* fs_path, bool relative_mode,
const char* relative_prefix, bool preserve_atimes,
bool preserve_crtimes, bool preserve_xattrs, bool preserve_crtimes, bool preserve_xattrs,
bool preserve_acls) { bool preserve_acls) {
if (!dir_entries || !root_path || !fs_path) if (!dir_entries || !root_path || !fs_path)
@@ -689,14 +750,30 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
free(rel); free(rel);
return true; return true;
} }
char* prefixed = NULL;
if (relative_prefix) {
prefixed = scanner_prefix_send_path(relative_prefix, rel);
if (!prefixed) {
free(rel);
return false;
}
if (prefixed[0] == '\0') {
/* -R with a cut at the receive root: the root itself has no wire path. */
free(prefixed);
free(rel);
return true;
}
}
File* file = file_create(fs_path); File* file = file_create(fs_path);
if (!file) { if (!file) {
free(prefixed);
free(rel); free(rel);
return false; return false;
} }
file->is_dir = true; file->is_dir = true;
file->metadata = file_metadata_create(fs_path, &st, preserve_atimes, preserve_crtimes); file->metadata = file_metadata_create(fs_path, &st, preserve_atimes, preserve_crtimes);
if (!file->metadata) { if (!file->metadata) {
free(prefixed);
free(rel); free(rel);
file_destroy(file); file_destroy(file);
return false; return false;
@@ -709,7 +786,11 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
if (relative_mode) { if (relative_mode) {
file->send_path = rel; file->send_path = rel;
rel = NULL; rel = NULL;
} else if (prefixed) {
file->send_path = prefixed;
prefixed = NULL;
} }
free(prefixed);
free(rel); free(rel);
bool added; bool added;
if (mutex) { if (mutex) {
@@ -807,9 +888,9 @@ static int open_next_directory(DirectoryScanner* scanner) {
if (scanner->options.capture_dir_times && if (scanner->options.capture_dir_times &&
!scanner_capture_dir_time( !scanner_capture_dir_time(
scanner->options.dir_entries, scanner->options.dir_entries_mutex, scanner->root_path, scanner->options.dir_entries, scanner->options.dir_entries_mutex, scanner->root_path,
scanner->current_path, scanner->relative_mode, scanner->options.preserve_atimes, scanner->current_path, scanner->relative_mode, scanner->options.relative_prefix,
scanner->options.preserve_crtimes, scanner->options.preserve_xattrs, scanner->options.preserve_atimes, scanner->options.preserve_crtimes,
scanner->options.preserve_acls)) { scanner->options.preserve_xattrs, scanner->options.preserve_acls)) {
closedir(scanner->current_dir); closedir(scanner->current_dir);
scanner->current_dir = NULL; scanner->current_dir = NULL;
free(scanner->current_path); free(scanner->current_path);
@@ -1175,14 +1256,28 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
wire paths are never recorded (see ScannerOptions.excluded_paths). */ wire paths are never recorded (see ScannerOptions.excluded_paths). */
bool files_from_prune = bool files_from_prune =
scanner->options.file_list && !file_list_affects(scanner->options.file_list, rel); scanner->options.file_list && !file_list_affects(scanner->options.file_list, rel);
if (!files_from_prune && !scanner->relative_mode) if (!files_from_prune && !scanner->relative_mode) {
if (scanner->options.relative_prefix) {
char* wrel = scanner_prefix_send_path(scanner->options.relative_prefix, rel);
if (!wrel) {
free(rel);
free(cur_path);
scanner->failed = true;
break;
}
scanner_record_excluded(scanner, wrel);
free(wrel);
} else {
scanner_record_excluded(scanner, cur_path); scanner_record_excluded(scanner, cur_path);
} }
/* With -R + --files-from the wire/destination path is the entry's bare }
relative path; keep `rel` alive to attach it to a transferred file. */ }
char* rel_copy = scanner->relative_mode ? str_dup(rel) : NULL; /* With -R the wire/destination path is a reconstructed relative path, not
the source path; keep `rel` alive to build it for a transferred file. */
bool needs_rel = scanner->relative_mode || scanner->options.relative_prefix != NULL;
char* rel_copy = needs_rel ? str_dup(rel) : NULL;
free(rel); free(rel);
if (rel_copy == NULL && scanner->relative_mode) { if (rel_copy == NULL && needs_rel) {
free(cur_path); free(cur_path);
scanner->failed = true; scanner->failed = true;
break; break;
@@ -1257,6 +1352,15 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
if (scanner->relative_mode) { if (scanner->relative_mode) {
file->send_path = rel_copy; file->send_path = rel_copy;
rel_copy = NULL; rel_copy = NULL;
} else if (scanner->options.relative_prefix) {
file->send_path = scanner_prefix_send_path(scanner->options.relative_prefix, rel_copy);
free(rel_copy);
rel_copy = NULL;
if (!file->send_path) {
file_destroy(file);
scanner->failed = true;
break;
}
} }
/* --devices/--specials: a device/FIFO/socket entry marked for preservation /* --devices/--specials: a device/FIFO/socket entry marked for preservation
becomes a node to recreate (is_special, no data, rdev captured); an becomes a node to recreate (is_special, no data, rdev captured); an
@@ -1536,6 +1640,15 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
if (options->relative && options->file_list != NULL) { if (options->relative && options->file_list != NULL) {
if (!excluded_sink_append(sink, options->excluded_mutex, entry->d_name)) if (!excluded_sink_append(sink, options->excluded_mutex, entry->d_name))
ps->failed = true; ps->failed = true;
} else if (options->relative_prefix) {
char* wrel = scanner_prefix_send_path(options->relative_prefix, entry->d_name);
if (!wrel) {
ps->failed = true;
} else {
if (!excluded_sink_append(sink, options->excluded_mutex, wrel))
ps->failed = true;
free(wrel);
}
} else { } else {
char* abs_path = path_cat(root_directory, entry->d_name); char* abs_path = path_cat(root_directory, entry->d_name);
if (!abs_path) { if (!abs_path) {
@@ -1577,13 +1690,13 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
return; return;
} }
if (is_dir) { if (is_dir) {
free(rel);
if (!scanner_same_filesystem(options->one_file_system, root_dev, st.st_dev)) { if (!scanner_same_filesystem(options->one_file_system, root_dev, st.st_dev)) {
/* -x/--one-file-system: emit the mount-point directory entry (empty) but /* -x/--one-file-system: emit the mount-point directory entry (empty) but
do not descend into it (see the sequential scanner for the same rule). */ do not descend into it (see the sequential scanner for the same rule). */
File* mount = file_create(cur_path); File* mount = file_create(cur_path);
free(cur_path); free(cur_path);
if (mount == NULL) { if (mount == NULL) {
free(rel);
ps->failed = true; ps->failed = true;
return; return;
} }
@@ -1592,17 +1705,29 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
mount->metadata = file_metadata_create(mount->path, &st, options->preserve_atimes, mount->metadata = file_metadata_create(mount->path, &st, options->preserve_atimes,
options->preserve_crtimes); options->preserve_crtimes);
if (!mount->metadata) { if (!mount->metadata) {
free(rel);
file_destroy(mount); file_destroy(mount);
ps->failed = true; ps->failed = true;
return; return;
} }
} }
if (options->relative_prefix) {
mount->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
if (!mount->send_path) {
free(rel);
file_destroy(mount);
ps->failed = true;
return;
}
}
free(rel);
if (!array_list_add(root_files, mount)) { if (!array_list_add(root_files, mount)) {
file_destroy(mount); file_destroy(mount);
ps->failed = true; ps->failed = true;
} }
return; return;
} }
free(rel);
if (!array_list_add(subdirs, cur_path)) { if (!array_list_add(subdirs, cur_path)) {
free(cur_path); free(cur_path);
ps->failed = true; ps->failed = true;
@@ -1628,6 +1753,15 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
if (use_rel) { if (use_rel) {
file->send_path = rel; file->send_path = rel;
rel = NULL; rel = NULL;
} else if (options->relative_prefix) {
file->send_path = scanner_prefix_send_path(options->relative_prefix, rel);
free(rel);
rel = NULL;
if (!file->send_path) {
file_destroy(file);
ps->failed = true;
return;
}
} }
ScannerSpecial special = scanner_prepare_special( ScannerSpecial special = scanner_prepare_special(
options->preserve_devices, options->preserve_specials, options->copy_devices, file, &st); options->preserve_devices, options->preserve_specials, options->copy_devices, file, &st);
@@ -1857,8 +1991,9 @@ ParallelScanner* parallel_scanner_create_with_options(const char* root_directory
if (options->capture_dir_times && if (options->capture_dir_times &&
!scanner_capture_dir_time(options->dir_entries, options->dir_entries_mutex, root_directory, !scanner_capture_dir_time(options->dir_entries, options->dir_entries_mutex, root_directory,
root_directory, options->relative && options->file_list != NULL, root_directory, options->relative && options->file_list != NULL,
options->preserve_atimes, options->preserve_crtimes, options->relative_prefix, options->preserve_atimes,
options->preserve_xattrs, options->preserve_acls)) { options->preserve_crtimes, options->preserve_xattrs,
options->preserve_acls)) {
array_list_delete(root_files); array_list_delete(root_files);
array_list_delete(subdirs); array_list_delete(subdirs);
parallel_scanner_destroy(ps); parallel_scanner_destroy(ps);
+12
View File
@@ -65,6 +65,11 @@ typedef struct {
bool per_dir_filters; /* -F: read .rsync-filter per directory */ bool per_dir_filters; /* -F: read .rsync-filter per directory */
bool dirs; /* -d/--dirs: transfer dir entries, no recursion */ bool dirs; /* -d/--dirs: transfer dir entries, no recursion */
bool relative; /* -R/--relative (dest rel paths, with --files-from) */ bool relative; /* -R/--relative (dest rel paths, with --files-from) */
/* -R/--relative outside --files-from: the destination-relative path prefix
* reconstructed from the source spec (rsync's '/./' cut point), or NULL when
* -R is off or --files-from is in use (the bare-relative path then comes from
* the listed entry). Borrowed read-only; owned by client_send. */
const char* relative_prefix;
/* --list-only: emit an is_dir File for every traversed directory (the listing /* --list-only: emit an is_dir File for every traversed directory (the listing
* includes directory entries, matching rsync). Client-only; never set on a * includes directory entries, matching rsync). Client-only; never set on a
* real transfer, which relies on implicit parent creation. */ * real transfer, which relies on implicit parent creation. */
@@ -213,6 +218,13 @@ bool scanner_same_filesystem(bool one_file_system, dev_t root_device, dev_t entr
* "/". Exposed so tests can exercise the mapping directly. */ * "/". Exposed so tests can exercise the mapping directly. */
char* scanner_path_relative(const char* root, const char* fs_path); char* scanner_path_relative(const char* root, const char* fs_path);
/* -R/--relative destination-relative prefix reconstructed from a source spec:
* the path after rsync's first '.' path component (the '/./' cut point), with
* leading/trailing slashes removed, or the whole spec (normalized) when there
* is no cut. Returns "" for the receive root, or NULL when `spec` is NULL or
* allocation fails. Exposed so tests can exercise the mapping directly. */
char* scanner_relative_prefix(const char* spec);
ParallelScanner* parallel_scanner_create_with_options(const char* root_directory, ParallelScanner* parallel_scanner_create_with_options(const char* root_directory,
const ScannerOptions* options, const ScannerOptions* options,
ProtocolSession* allocation_session); ProtocolSession* allocation_session);
+54
View File
@@ -1069,6 +1069,59 @@ static void test_scanner_path_relative() {
EXPECT_NULL(scanner_path_relative("/tmp/foo", "/tmp/foobar")); EXPECT_NULL(scanner_path_relative("/tmp/foo", "/tmp/foobar"));
} }
/* -R/--relative destination prefix: the '/./' cut point and normalization. */
static void test_scanner_relative_prefix() {
char* p = NULL;
/* No cut: the whole spec with leading/trailing slashes removed. */
p = scanner_relative_prefix("/tmp/src/foo/");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "tmp/src/foo");
free(p);
p = scanner_relative_prefix("src/foo");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "src/foo");
free(p);
/* Trailing "/." is the directory itself, not a cut. */
p = scanner_relative_prefix("src/foo/.");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "src/foo");
free(p);
/* The first "/./" cuts everything before it. */
p = scanner_relative_prefix("/a/./b/c");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "b/c");
free(p);
p = scanner_relative_prefix("src/./");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "");
free(p);
/* A later "." component is normalized away. */
p = scanner_relative_prefix("a/./b/./c");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "b/c");
free(p);
/* A leading "./" is the cut at the start. */
p = scanner_relative_prefix("./s2");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "s2");
free(p);
p = scanner_relative_prefix(".");
EXPECT_NOT_NULL(p);
EXPECT_EQ_STR(p, "");
free(p);
EXPECT_NULL(scanner_relative_prefix(NULL));
EXPECT_NULL(scanner_relative_prefix(""));
}
/* rsync precedence: a deeper .rsync-filter overrides a shallower one, so an /* rsync precedence: a deeper .rsync-filter overrides a shallower one, so an
* inner "+ *.tmp" re-includes what the outer "- *.tmp" excluded. */ * inner "+ *.tmp" re-includes what the outer "- *.tmp" excluded. */
static void test_per_dir_filter_override(bool parallel) { static void test_per_dir_filter_override(bool parallel) {
@@ -1607,6 +1660,7 @@ void test_scanner() {
test_per_dir_filter(false); test_per_dir_filter(false);
test_per_dir_filter(true); test_per_dir_filter(true);
test_scanner_path_relative(); test_scanner_path_relative();
test_scanner_relative_prefix();
test_per_dir_filter_override(false); test_per_dir_filter_override(false);
test_per_dir_filter_override(true); test_per_dir_filter_override(true);
test_dirs_no_descent(); test_dirs_no_descent();