Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b4164d3ba | ||
|
|
e093a3c5b6 |
@@ -465,7 +465,7 @@ defaults to the current directory. |
|
|||||||
|
|
||||||
## Protocol and Security
|
## Protocol and Security
|
||||||
|
|
||||||
FastSync protocol version `2.3.0` is shared by the client and server. The
|
FastSync protocol version `2.2.0` is shared by the client and server. The
|
||||||
current protocol is sender-driven and includes configuration negotiation,
|
current protocol is sender-driven and includes configuration negotiation,
|
||||||
incremental checks, checksums, manifests, keep-alives, abort handling, and
|
incremental checks, checksums, manifests, keep-alives, abort handling, and
|
||||||
FastSync-native delta messages. Client and server versions must currently
|
FastSync-native delta messages. Client and server versions must currently
|
||||||
|
|||||||
+1
-1
@@ -120,7 +120,7 @@ This document maps rsync's full feature set to FastSync's current implementation
|
|||||||
| `-g`, `--group` | Preserve group | ✅ Implemented | Part of -M |
|
| `-g`, `--group` | Preserve group | ✅ Implemented | Part of -M |
|
||||||
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
|
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
|
||||||
| `-E`, `--executability` | Preserve executability | ❌ Not Implemented | |
|
| `-E`, `--executability` | Preserve executability | ❌ Not Implemented | |
|
||||||
| `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | |
|
| `--chmod=CHMOD` | Affect file permissions | ✅ Implemented | Supports numeric and symbolic `ugo` `rwx` changes; retains receiver safety masking |
|
||||||
| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect |
|
| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect |
|
||||||
| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect |
|
| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect |
|
||||||
| `-H`, `--hard-links` | Preserve hard links | ❌ Not Implemented | Removed because it had no effect |
|
| `-H`, `--hard-links` | Preserve hard links | ❌ Not Implemented | Removed because it had no effect |
|
||||||
|
|||||||
+22
-1
@@ -1,5 +1,6 @@
|
|||||||
#include "client_send.h"
|
#include "client_send.h"
|
||||||
#include "client_validation.h"
|
#include "client_validation.h"
|
||||||
|
#include "chmod.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "delta.h"
|
#include "delta.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
@@ -153,7 +154,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
|
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
|
||||||
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
|
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
|
||||||
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
|
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
|
||||||
{"--ignore-existing", NULL, OPT_FLAG, offsetof(Config, ignore_existing)},
|
{"--chmod", NULL, OPT_STRING, offsetof(Config, chmod_spec)},
|
||||||
|
|
||||||
{"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
|
{"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
|
||||||
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
|
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
|
||||||
@@ -221,12 +222,32 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
|||||||
}
|
}
|
||||||
if (apply_table_option(config, entry, argv[++i]) != 0)
|
if (apply_table_option(config, entry, argv[++i]) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
if (entry->offset == offsetof(Config, chmod_spec)) {
|
||||||
|
mode_t ignored;
|
||||||
|
if (!chmod_apply(0, config->chmod_spec, &ignored)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--chmod has invalid permission changes");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
config->use_metadata = true;
|
||||||
|
}
|
||||||
} else if (apply_table_option(config, entry, NULL) != 0) {
|
} else if (apply_table_option(config, entry, NULL) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strncmp(argv[i], "--chmod=", 8) == 0) {
|
||||||
|
if (set_string_option(&config->chmod_spec, argv[i] + 8, "--chmod") != 0)
|
||||||
|
return -1;
|
||||||
|
mode_t ignored;
|
||||||
|
if (!chmod_apply(0, config->chmod_spec, &ignored)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "--chmod has invalid permission changes");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
config->use_metadata = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (opt_is(argv[i], "--help", NULL)) {
|
if (opt_is(argv[i], "--help", NULL)) {
|
||||||
print_usage();
|
print_usage();
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,6 @@ void print_usage(void) {
|
|||||||
printf(" -p <port> SSH port (default: 22)\n");
|
printf(" -p <port> SSH port (default: 22)\n");
|
||||||
printf(" --progress Show transfer progress\n");
|
printf(" --progress Show transfer progress\n");
|
||||||
printf(" --delete Delete files on receiver not in source\n");
|
printf(" --delete Delete files on receiver not in source\n");
|
||||||
printf(" --ignore-existing Skip files that already exist on receiver\n");
|
|
||||||
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
||||||
printf(" --include <pattern> Only include files matching pattern\n");
|
printf(" --include <pattern> Only include files matching pattern\n");
|
||||||
printf(" --exclude-from <file> Read exclude patterns from file\n");
|
printf(" --exclude-from <file> Read exclude patterns from file\n");
|
||||||
@@ -39,6 +38,7 @@ void print_usage(void) {
|
|||||||
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
|
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
|
||||||
printf(" -v, --verbose Enable debug logging\n");
|
printf(" -v, --verbose Enable debug logging\n");
|
||||||
printf(" -M, --preserve Preserve file metadata\n");
|
printf(" -M, --preserve Preserve file metadata\n");
|
||||||
|
printf(" --chmod <changes> Modify transferred permissions (rsync syntax)\n");
|
||||||
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
|
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
|
||||||
printf(" --source-dir <path> Source directory\n");
|
printf(" --source-dir <path> Source directory\n");
|
||||||
printf(" --dest-dir <path> Destination directory\n");
|
printf(" --dest-dir <path> Destination directory\n");
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include "chmod.h"
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static bool parse_clause(mode_t* mode, const char* begin, const char* end) {
|
||||||
|
const char* p = begin;
|
||||||
|
unsigned who = 0;
|
||||||
|
while (p < end && strchr("ugoa", *p)) {
|
||||||
|
if (*p == 'a')
|
||||||
|
who = 7;
|
||||||
|
else
|
||||||
|
who |= *p == 'u' ? 1U : (*p == 'g' ? 2U : 4U);
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (who == 0)
|
||||||
|
who = 7;
|
||||||
|
if (p == end || (*p != '+' && *p != '-' && *p != '='))
|
||||||
|
return false;
|
||||||
|
char operation = *p++;
|
||||||
|
mode_t bits = 0;
|
||||||
|
while (p < end) {
|
||||||
|
mode_t bit;
|
||||||
|
switch (*p++) {
|
||||||
|
case 'r':
|
||||||
|
bit = 4;
|
||||||
|
break;
|
||||||
|
case 'w':
|
||||||
|
bit = 2;
|
||||||
|
break;
|
||||||
|
case 'x':
|
||||||
|
bit = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bits |= bit;
|
||||||
|
}
|
||||||
|
for (unsigned class_index = 0; class_index < 3; class_index++) {
|
||||||
|
unsigned class_bit = 1U << class_index;
|
||||||
|
if (!(who & class_bit))
|
||||||
|
continue;
|
||||||
|
mode_t shift = (mode_t)((2U - class_index) * 3U);
|
||||||
|
mode_t mask = (mode_t)(7U << shift);
|
||||||
|
mode_t class_bits = (mode_t)(bits << shift);
|
||||||
|
if (operation == '+')
|
||||||
|
*mode |= class_bits;
|
||||||
|
else if (operation == '-')
|
||||||
|
*mode &= ~class_bits;
|
||||||
|
else
|
||||||
|
*mode = (*mode & ~mask) | class_bits;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool chmod_apply(mode_t mode, const char* spec, mode_t* result) {
|
||||||
|
if (!spec || !*spec || !result)
|
||||||
|
return false;
|
||||||
|
bool numeric = true;
|
||||||
|
size_t length = strlen(spec);
|
||||||
|
if (length > 4)
|
||||||
|
numeric = false;
|
||||||
|
for (size_t i = 0; i < length && numeric; i++)
|
||||||
|
numeric = spec[i] >= '0' && spec[i] <= '7';
|
||||||
|
if (numeric) {
|
||||||
|
if (length == 0 || length > 4)
|
||||||
|
return false;
|
||||||
|
mode_t parsed = 0;
|
||||||
|
for (size_t i = 0; i < length; i++)
|
||||||
|
parsed = (mode_t)((parsed << 3) | (spec[i] - '0'));
|
||||||
|
*result = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_t changed = mode;
|
||||||
|
const char* begin = spec;
|
||||||
|
while (*begin) {
|
||||||
|
const char* end = strchr(begin, ',');
|
||||||
|
if (!end)
|
||||||
|
end = begin + strlen(begin);
|
||||||
|
if (!parse_clause(&changed, begin, end))
|
||||||
|
return false;
|
||||||
|
if (*end == '\0')
|
||||||
|
break;
|
||||||
|
begin = end + 1;
|
||||||
|
if (!*begin)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*result = changed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#ifndef CHMOD_H
|
||||||
|
#define CHMOD_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
/* Apply the supported rsync --chmod syntax to a permission mode. */
|
||||||
|
bool chmod_apply(mode_t mode, const char* spec, mode_t* result);
|
||||||
|
|
||||||
|
#endif
|
||||||
+23
-17
@@ -1,4 +1,5 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "chmod.h"
|
||||||
#include "delta.h"
|
#include "delta.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
@@ -68,7 +69,6 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->debug_level = 0;
|
config->debug_level = 0;
|
||||||
config->list_only = false;
|
config->list_only = false;
|
||||||
config->human_readable = false;
|
config->human_readable = false;
|
||||||
config->ignore_existing = false;
|
|
||||||
config->update = false;
|
config->update = false;
|
||||||
config->inplace = false;
|
config->inplace = false;
|
||||||
config->append = false;
|
config->append = false;
|
||||||
@@ -99,6 +99,7 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->server_mode = false;
|
config->server_mode = false;
|
||||||
config->checksum = false;
|
config->checksum = false;
|
||||||
config->compress_choice = NULL;
|
config->compress_choice = NULL;
|
||||||
|
config->chmod_spec = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool valid_wire_bool(int value) {
|
static bool valid_wire_bool(int value) {
|
||||||
@@ -124,19 +125,20 @@ static bool validate_received_config(const Config* config) {
|
|||||||
valid_wire_bool(config->copy_unsafe_links) &&
|
valid_wire_bool(config->copy_unsafe_links) &&
|
||||||
valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) &&
|
valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) &&
|
||||||
valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) &&
|
valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) &&
|
||||||
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->ignore_existing) &&
|
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->update) &&
|
||||||
valid_wire_bool(config->update) && valid_wire_bool(config->inplace) &&
|
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
||||||
valid_wire_bool(config->append) && valid_wire_bool(config->append_verify) &&
|
valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) &&
|
||||||
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) &&
|
valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) &&
|
||||||
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) &&
|
||||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) &&
|
||||||
valid_wire_bool(config->checksum) &&
|
|
||||||
(!config->use_compression ||
|
(!config->use_compression ||
|
||||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||||
config->delta_block_size >= DELTA_BLOCK_SIZE_MIN &&
|
config->delta_block_size >= DELTA_BLOCK_SIZE_MIN &&
|
||||||
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
|
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
|
||||||
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->max_delete >= 0;
|
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->max_delete >= 0 &&
|
||||||
|
(!config->chmod_spec || !*config->chmod_spec ||
|
||||||
|
chmod_apply(0, config->chmod_spec, &(mode_t){0}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Config* config_create(void) {
|
Config* config_create(void) {
|
||||||
@@ -210,6 +212,7 @@ void config_delete(Config* config) {
|
|||||||
free(config->bind_address);
|
free(config->bind_address);
|
||||||
free(config->daemon_config);
|
free(config->daemon_config);
|
||||||
free(config->compress_choice);
|
free(config->compress_choice);
|
||||||
|
free(config->chmod_spec);
|
||||||
if (config->filters) {
|
if (config->filters) {
|
||||||
array_list_delete(config->filters);
|
array_list_delete(config->filters);
|
||||||
}
|
}
|
||||||
@@ -245,18 +248,18 @@ static bool send_file_options(int fd, const Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool send_selection_options(int fd, const Config* c) {
|
static bool send_selection_options(int fd, const Config* c) {
|
||||||
return send_int(fd, c->ignore_existing) && send_int(fd, c->update) && send_int(fd, c->inplace) &&
|
return send_int(fd, c->update) && send_int(fd, c->inplace) && send_int(fd, c->append) &&
|
||||||
send_int(fd, c->append) && send_int(fd, c->append_verify) &&
|
send_int(fd, c->append_verify) && send_int(fd, c->delete_excluded) &&
|
||||||
send_int(fd, c->delete_excluded) && send_int(fd, c->delete_after) &&
|
send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) &&
|
||||||
send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && send_int(fd, c->relative) &&
|
send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs);
|
||||||
send_int(fd, c->prune_empty_dirs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool send_resume_options(int fd, const Config* c) {
|
static bool send_resume_options(int fd, const Config* c) {
|
||||||
return send_str(fd, c->temp_dir ? c->temp_dir : "") && send_int(fd, c->partial) &&
|
return send_str(fd, c->temp_dir ? c->temp_dir : "") && send_int(fd, c->partial) &&
|
||||||
send_str(fd, c->partial_dir ? c->partial_dir : "") &&
|
send_str(fd, c->partial_dir ? c->partial_dir : "") &&
|
||||||
send_str(fd, c->suffix ? c->suffix : "") && send_int(fd, c->delete_before) &&
|
send_str(fd, c->suffix ? c->suffix : "") && send_int(fd, c->delete_before) &&
|
||||||
send_int(fd, c->checksum) && send_str(fd, c->compress_choice ? c->compress_choice : "");
|
send_int(fd, c->checksum) && send_str(fd, c->compress_choice ? c->compress_choice : "") &&
|
||||||
|
send_str(fd, c->chmod_spec ? c->chmod_spec : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_core_fields(int fd, Config* c) {
|
static bool receive_core_fields(int fd, Config* c) {
|
||||||
@@ -307,7 +310,7 @@ static bool receive_file_options(int fd, Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_selection_options(int fd, Config* c) {
|
static bool receive_selection_options(int fd, Config* c) {
|
||||||
bool* flags[] = {&c->ignore_existing, &c->update, &c->inplace, &c->append,
|
bool* flags[] = {&c->update, &c->inplace, &c->append,
|
||||||
&c->append_verify, &c->delete_excluded, &c->delete_after};
|
&c->append_verify, &c->delete_excluded, &c->delete_after};
|
||||||
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
||||||
if (!receive_wire_bool(fd, flags[i]))
|
if (!receive_wire_bool(fd, flags[i]))
|
||||||
@@ -333,7 +336,10 @@ static bool receive_resume_options(int fd, Config* c) {
|
|||||||
if (!receive_wire_bool(fd, &c->checksum))
|
if (!receive_wire_bool(fd, &c->checksum))
|
||||||
return false;
|
return false;
|
||||||
c->compress_choice = receive_str(fd);
|
c->compress_choice = receive_str(fd);
|
||||||
return c->compress_choice != NULL;
|
if (!c->compress_choice)
|
||||||
|
return false;
|
||||||
|
c->chmod_spec = receive_str(fd);
|
||||||
|
return c->chmod_spec != NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool config_send(int file_descriptor, const Config* config) {
|
bool config_send(int file_descriptor, const Config* config) {
|
||||||
|
|||||||
+1
-1
@@ -76,7 +76,6 @@ typedef struct Config {
|
|||||||
bool human_readable;
|
bool human_readable;
|
||||||
|
|
||||||
// Issue #127: Transfer modes
|
// Issue #127: Transfer modes
|
||||||
bool ignore_existing;
|
|
||||||
bool update;
|
bool update;
|
||||||
bool inplace;
|
bool inplace;
|
||||||
bool append;
|
bool append;
|
||||||
@@ -127,6 +126,7 @@ typedef struct Config {
|
|||||||
|
|
||||||
// PR #184: Compression algorithm negotiation
|
// PR #184: Compression algorithm negotiation
|
||||||
char* compress_choice;
|
char* compress_choice;
|
||||||
|
char* chmod_spec;
|
||||||
} Config;
|
} Config;
|
||||||
|
|
||||||
#define PROTOCOL_VERSION "2.3.0"
|
#define PROTOCOL_VERSION "2.3.0"
|
||||||
|
|||||||
+4
-37
@@ -172,17 +172,8 @@ bool file_set_authorized_root(int fd, const char* canonical_path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool file_path_exists_secure(const char* path) {
|
bool file_path_exists_secure(const char* path) {
|
||||||
if (!path)
|
|
||||||
return false;
|
|
||||||
char* leaf = NULL;
|
|
||||||
int parent_fd = file_open_secure_parent(path, &leaf, false);
|
|
||||||
if (parent_fd < 0)
|
|
||||||
return false;
|
|
||||||
struct stat st;
|
struct stat st;
|
||||||
bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0;
|
return file_stat_secure(path, &st);
|
||||||
close(parent_fd);
|
|
||||||
free(leaf);
|
|
||||||
return exists;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool file_stat_secure(const char* path, struct stat* st) {
|
bool file_stat_secure(const char* path, struct stat* st) {
|
||||||
@@ -311,9 +302,8 @@ bool file_rename_secure(const char* old_path, const char* new_path) {
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool file_to_disk_secure_impl(const char* path, const void* data,
|
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||||
unsigned long long data_size, bool inplace, bool sparse,
|
bool inplace, bool sparse, const FileMetadata* metadata) {
|
||||||
const FileMetadata* metadata, bool no_replace) {
|
|
||||||
char* leaf = NULL;
|
char* leaf = NULL;
|
||||||
int dirfd = file_open_secure_parent(path, &leaf, true);
|
int dirfd = file_open_secure_parent(path, &leaf, true);
|
||||||
if (dirfd < 0)
|
if (dirfd < 0)
|
||||||
@@ -344,20 +334,8 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
|||||||
if (close(fd) != 0)
|
if (close(fd) != 0)
|
||||||
ok = false;
|
ok = false;
|
||||||
fd = -1;
|
fd = -1;
|
||||||
if (ok) {
|
if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0)
|
||||||
if (no_replace) {
|
|
||||||
/* The probe and commit cannot be one operation. A concurrent
|
|
||||||
creator may win; EEXIST is then the requested skip. */
|
|
||||||
if (linkat(dirfd, tmp, dirfd, leaf, 0) == 0 || errno == EEXIST) {
|
|
||||||
if (unlinkat(dirfd, tmp, 0) != 0 && errno != ENOENT)
|
|
||||||
ok = false;
|
ok = false;
|
||||||
} else {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
} else if (renameat(dirfd, tmp, dirfd, leaf) != 0) {
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ok)
|
if (!ok)
|
||||||
unlinkat(dirfd, tmp, 0);
|
unlinkat(dirfd, tmp, 0);
|
||||||
}
|
}
|
||||||
@@ -369,17 +347,6 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
|
||||||
bool inplace, bool sparse, const FileMetadata* metadata) {
|
|
||||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, metadata, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
|
||||||
unsigned long long data_size, bool sparse,
|
|
||||||
const FileMetadata* metadata) {
|
|
||||||
return file_to_disk_secure_impl(path, data, data_size, false, sparse, metadata, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
||||||
bool inplace, bool sparse) {
|
bool inplace, bool sparse) {
|
||||||
if (!path || (!data && data_size != 0) || has_path_traversal(path))
|
if (!path || (!data && data_size != 0) || has_path_traversal(path))
|
||||||
|
|||||||
@@ -32,8 +32,5 @@ bool file_ensure_directory_secure(const char* path);
|
|||||||
bool file_rename_secure(const char* old_path, const char* new_path);
|
bool file_rename_secure(const char* old_path, const char* new_path);
|
||||||
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||||
bool inplace, bool sparse, const FileMetadata* metadata);
|
bool inplace, bool sparse, const FileMetadata* metadata);
|
||||||
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
|
||||||
unsigned long long data_size, bool sparse,
|
|
||||||
const FileMetadata* metadata);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+12
-22
@@ -8,6 +8,7 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
|
#include "chmod.h"
|
||||||
#include "compression.h"
|
#include "compression.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "data.h"
|
#include "data.h"
|
||||||
@@ -22,9 +23,7 @@
|
|||||||
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
|
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
|
||||||
|
|
||||||
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
|
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
|
||||||
/* Backups are incompatible with ignore-existing: moving the entry first
|
bool backup_enabled = config && config->backup;
|
||||||
would make a concurrent no-replace commit overwrite its old name. */
|
|
||||||
bool backup_enabled = config && config->backup && !config->ignore_existing;
|
|
||||||
bool inplace = config && config->inplace;
|
bool inplace = config && config->inplace;
|
||||||
bool sparse = config && config->preserve_sparse;
|
bool sparse = config && config->preserve_sparse;
|
||||||
const char* backup_suffix = (config && config->suffix) ? config->suffix : "~";
|
const char* backup_suffix = (config && config->suffix) ? config->suffix : "~";
|
||||||
@@ -63,20 +62,6 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --ignore-existing checks the final destination before partial files or
|
|
||||||
overwrite policies can modify it. */
|
|
||||||
if (config && config->ignore_existing) {
|
|
||||||
char* final_path = path_cat(root_directory, file->path);
|
|
||||||
bool exists = final_path && file_path_exists_secure(final_path);
|
|
||||||
free(final_path);
|
|
||||||
if (exists) {
|
|
||||||
free(confined_backup);
|
|
||||||
free(confined_partial);
|
|
||||||
free(disk_path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --update is receiver-side policy: never replace a newer destination. */
|
/* --update is receiver-side policy: never replace a newer destination. */
|
||||||
if (config && config->update) {
|
if (config && config->update) {
|
||||||
struct stat destination_stat;
|
struct stat destination_stat;
|
||||||
@@ -119,11 +104,16 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = config && config->ignore_existing
|
FileMetadata adjusted_metadata;
|
||||||
? file_to_disk_secure_no_replace(disk_path, file->data->data, file->data->size,
|
const FileMetadata* metadata = file->metadata;
|
||||||
sparse, file->metadata)
|
if (metadata && config && config->chmod_spec && *config->chmod_spec) {
|
||||||
: file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace,
|
adjusted_metadata = *metadata;
|
||||||
sparse, file->metadata);
|
if (!chmod_apply(adjusted_metadata.mode, config->chmod_spec, &adjusted_metadata.mode))
|
||||||
|
goto fail;
|
||||||
|
metadata = &adjusted_metadata;
|
||||||
|
}
|
||||||
|
bool ok =
|
||||||
|
file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse, metadata);
|
||||||
free(parent_copy);
|
free(parent_copy);
|
||||||
free(backup_path);
|
free(backup_path);
|
||||||
free(confined_backup);
|
free(confined_backup);
|
||||||
|
|||||||
@@ -52,6 +52,22 @@ class TestArchiveMode:
|
|||||||
assert not mismatches, f"Mismatch: {mismatches}"
|
assert not mismatches, f"Mismatch: {mismatches}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestChmod:
|
||||||
|
def test_chmod_applies_to_transferred_files(self, shared_server):
|
||||||
|
clean_dir(DEST_DIR)
|
||||||
|
source_file = os.path.join(SOURCE_DIR, "small.txt")
|
||||||
|
os.chmod(source_file, 0o777)
|
||||||
|
result, dur = run_client(
|
||||||
|
SOURCE_DIR, DEST_DIR,
|
||||||
|
flags=["--chmod=u=rw,go=r"],
|
||||||
|
port=shared_server.port,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||||
|
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||||
|
assert (os.stat(os.path.join(received, "small.txt")).st_mode & 0o777) == 0o644
|
||||||
|
|
||||||
|
|
||||||
class TestExclude:
|
class TestExclude:
|
||||||
def test_exclude_single(self, shared_server):
|
def test_exclude_single(self, shared_server):
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
@@ -214,33 +230,6 @@ class TestIncremental:
|
|||||||
assert f.read() == b"hello world\n"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
|
||||||
class TestIgnoreExisting:
|
|
||||||
def test_ignore_existing_preserves_existing_and_transfers_new(self, shared_server):
|
|
||||||
clean_dir(DEST_DIR)
|
|
||||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, port=shared_server.port)
|
|
||||||
assert result.returncode == 0
|
|
||||||
|
|
||||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
|
||||||
existing_file = os.path.join(received, "small.txt")
|
|
||||||
with open(existing_file, "wb") as f:
|
|
||||||
f.write(b"destination content\n")
|
|
||||||
new_source = os.path.join(SOURCE_DIR, "new.txt")
|
|
||||||
try:
|
|
||||||
with open(new_source, "wb") as f:
|
|
||||||
f.write(b"new file\n")
|
|
||||||
|
|
||||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
|
||||||
flags=["--ignore-existing"], port=shared_server.port)
|
|
||||||
assert result.returncode == 0, f"Sync failed: {(result.stderr or result.stdout)[:200]}"
|
|
||||||
with open(existing_file, "rb") as f:
|
|
||||||
assert f.read() == b"destination content\n"
|
|
||||||
with open(os.path.join(received, "new.txt"), "rb") as f:
|
|
||||||
assert f.read() == b"new file\n"
|
|
||||||
finally:
|
|
||||||
if os.path.lexists(new_source):
|
|
||||||
os.unlink(new_source)
|
|
||||||
|
|
||||||
|
|
||||||
class TestDelete:
|
class TestDelete:
|
||||||
def test_delete_removes_extra_files(self, shared_server):
|
def test_delete_removes_extra_files(self, shared_server):
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
|
|||||||
+30
-6
@@ -1,5 +1,6 @@
|
|||||||
#include "test_client_cli.h"
|
#include "test_client_cli.h"
|
||||||
#include "client_validation.h"
|
#include "client_validation.h"
|
||||||
|
#include "chmod.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
@@ -181,16 +182,37 @@ static void test_parse_args_valid_port() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_parse_args_ignore_existing() {
|
static void test_parse_args_chmod() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
char* argv[] = {"fastsync", "--ignore-existing", "/src", "/dst"};
|
char* argv[] = {"fastsync", "--chmod=u=rw,go=r", "/src", "/dst"};
|
||||||
int positional_args[2];
|
int positional_args[2];
|
||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_STR(cfg->chmod_spec, "u=rw,go=r");
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
mode_t result;
|
||||||
|
EXPECT_TRUE(chmod_apply(0777, cfg->chmod_spec, &result));
|
||||||
|
EXPECT_EQ_INT(result, 0644);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
int ret = parse_args(cfg, 4, argv, positional_args, &positional_count);
|
static void test_parse_args_numeric_chmod() {
|
||||||
EXPECT_EQ_INT(ret, 0);
|
Config* cfg = config_create();
|
||||||
EXPECT_TRUE(cfg->ignore_existing);
|
char* argv[] = {"fastsync", "--chmod", "7777", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||||
|
EXPECT_EQ_STR(cfg->chmod_spec, "7777");
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_rejects_invalid_chmod() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--chmod=a+X", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +387,9 @@ void test_client_cli() {
|
|||||||
test_parse_args_help();
|
test_parse_args_help();
|
||||||
test_parse_args_version();
|
test_parse_args_version();
|
||||||
test_parse_args_valid_port();
|
test_parse_args_valid_port();
|
||||||
test_parse_args_ignore_existing();
|
test_parse_args_chmod();
|
||||||
|
test_parse_args_numeric_chmod();
|
||||||
|
test_parse_args_rejects_invalid_chmod();
|
||||||
test_parse_args_invalid_port();
|
test_parse_args_invalid_port();
|
||||||
test_parse_args_non_numeric_port();
|
test_parse_args_non_numeric_port();
|
||||||
test_parse_args_invalid_server_port();
|
test_parse_args_invalid_server_port();
|
||||||
|
|||||||
+2
-5
@@ -126,7 +126,6 @@ static void test_config_send_receive() {
|
|||||||
send_cfg->use_metadata = true;
|
send_cfg->use_metadata = true;
|
||||||
send_cfg->compression_level = 5;
|
send_cfg->compression_level = 5;
|
||||||
send_cfg->chunk_size = 1024;
|
send_cfg->chunk_size = 1024;
|
||||||
send_cfg->ignore_existing = true;
|
|
||||||
|
|
||||||
/* Use socketpair for bidirectional communication */
|
/* Use socketpair for bidirectional communication */
|
||||||
int p[2];
|
int p[2];
|
||||||
@@ -161,8 +160,6 @@ static void test_config_send_receive() {
|
|||||||
ok = false;
|
ok = false;
|
||||||
if (recv_cfg->chunk_size != 1024)
|
if (recv_cfg->chunk_size != 1024)
|
||||||
ok = false;
|
ok = false;
|
||||||
if (!recv_cfg->ignore_existing)
|
|
||||||
ok = false;
|
|
||||||
}
|
}
|
||||||
config_delete(recv_cfg);
|
config_delete(recv_cfg);
|
||||||
close(p[0]);
|
close(p[0]);
|
||||||
@@ -188,11 +185,11 @@ static void test_config_send_receive() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void test_config_send_receive_version_mismatch() {
|
static void test_config_send_receive_version_mismatch() {
|
||||||
/* A peer using the previous wire format must be rejected. */
|
/* Create a config with a different protocol version */
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
EXPECT_NOT_NULL(cfg);
|
EXPECT_NOT_NULL(cfg);
|
||||||
free(cfg->version);
|
free(cfg->version);
|
||||||
cfg->version = str_dup("2.2.0");
|
cfg->version = str_dup("0.0");
|
||||||
cfg->send_directory = str_dup("/src");
|
cfg->send_directory = str_dup("/src");
|
||||||
cfg->receive_root_directory = str_dup("/dst");
|
cfg->receive_root_directory = str_dup("/dst");
|
||||||
|
|
||||||
|
|||||||
@@ -88,99 +88,6 @@ static void test_file_save_to_disk() {
|
|||||||
rmdir("test_save_tmp");
|
rmdir("test_save_tmp");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_file_save_to_disk_ignore_existing() {
|
|
||||||
const char* path = "test_ignore_existing_tmp/existing.txt";
|
|
||||||
EXPECT_TRUE(file_write_to_disk(path, "old", 3, false, false));
|
|
||||||
|
|
||||||
File* file = file_create("existing.txt");
|
|
||||||
EXPECT_NOT_NULL(file);
|
|
||||||
file->data->data = malloc(3);
|
|
||||||
EXPECT_NOT_NULL(file->data->data);
|
|
||||||
memcpy(file->data->data, "new", 3);
|
|
||||||
file->data->size = 3;
|
|
||||||
|
|
||||||
Config* config = config_create();
|
|
||||||
EXPECT_NOT_NULL(config);
|
|
||||||
config->ignore_existing = true;
|
|
||||||
EXPECT_TRUE(file_save_to_disk("test_ignore_existing_tmp", file, config));
|
|
||||||
|
|
||||||
FILE* stream = fopen(path, "rb");
|
|
||||||
char content[4] = {0};
|
|
||||||
EXPECT_NOT_NULL(stream);
|
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
|
||||||
if (stream) {
|
|
||||||
EXPECT_EQ_INT((int)fread(content, 1, 3, stream), 3);
|
|
||||||
fclose(stream);
|
|
||||||
}
|
|
||||||
EXPECT_EQ_STR(content, "old");
|
|
||||||
|
|
||||||
file_destroy(file);
|
|
||||||
config_delete(config);
|
|
||||||
unlink(path);
|
|
||||||
rmdir("test_ignore_existing_tmp");
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_file_save_to_disk_ignore_existing_entry_types() {
|
|
||||||
const char* root = "test_ignore_existing_entries_tmp";
|
|
||||||
const char* directory = "test_ignore_existing_entries_tmp/directory";
|
|
||||||
const char* link = "test_ignore_existing_entries_tmp/link";
|
|
||||||
const char* target = "test_ignore_existing_entries_tmp/target";
|
|
||||||
const char* backup = "test_ignore_existing_entries_tmp/backup.txt~";
|
|
||||||
const char* backup_file = "test_ignore_existing_entries_tmp/backup.txt";
|
|
||||||
Config* config = config_create();
|
|
||||||
File* file = file_create("unused");
|
|
||||||
|
|
||||||
unlink(link);
|
|
||||||
unlink(target);
|
|
||||||
unlink(backup);
|
|
||||||
unlink(backup_file);
|
|
||||||
rmdir(directory);
|
|
||||||
rmdir(root);
|
|
||||||
EXPECT_NOT_NULL(config);
|
|
||||||
EXPECT_NOT_NULL(file);
|
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
|
||||||
if (!config || !file)
|
|
||||||
return;
|
|
||||||
config->ignore_existing = true;
|
|
||||||
config->backup = true;
|
|
||||||
file->data->data = malloc(3);
|
|
||||||
EXPECT_NOT_NULL(file->data->data);
|
|
||||||
// cppcheck-suppress knownConditionTrueFalse
|
|
||||||
if (!file->data->data) {
|
|
||||||
file_destroy(file);
|
|
||||||
config_delete(config);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
memcpy(file->data->data, "new", 3);
|
|
||||||
file->data->size = 3;
|
|
||||||
|
|
||||||
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
|
||||||
EXPECT_EQ_INT(mkdir(directory, 0755), 0);
|
|
||||||
EXPECT_TRUE(file_write_to_disk(target, "old", 3, false, false));
|
|
||||||
EXPECT_EQ_INT(symlink("target", link), 0);
|
|
||||||
free(file->path);
|
|
||||||
file->path = str_dup("directory");
|
|
||||||
EXPECT_TRUE(file_save_to_disk(root, file, config));
|
|
||||||
free(file->path);
|
|
||||||
file->path = str_dup("link");
|
|
||||||
EXPECT_TRUE(file_save_to_disk(root, file, config));
|
|
||||||
|
|
||||||
free(file->path);
|
|
||||||
file->path = str_dup("backup.txt");
|
|
||||||
EXPECT_TRUE(file_write_to_disk(backup_file, "old", 3, false, false));
|
|
||||||
EXPECT_TRUE(file_save_to_disk(root, file, config));
|
|
||||||
EXPECT_TRUE(file_path_exists_secure(backup_file));
|
|
||||||
EXPECT_FALSE(file_path_exists_secure(backup));
|
|
||||||
|
|
||||||
file_destroy(file);
|
|
||||||
config_delete(config);
|
|
||||||
unlink(link);
|
|
||||||
unlink(target);
|
|
||||||
unlink(backup_file);
|
|
||||||
rmdir(directory);
|
|
||||||
rmdir(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_file_write_to_disk_basic() {
|
static void test_file_write_to_disk_basic() {
|
||||||
const char* content = "Basic file_write_to_disk test";
|
const char* content = "Basic file_write_to_disk test";
|
||||||
EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content),
|
EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content),
|
||||||
@@ -551,8 +458,6 @@ void test_file() {
|
|||||||
test_file_load_data();
|
test_file_load_data();
|
||||||
test_file_load_data_missing_file();
|
test_file_load_data_missing_file();
|
||||||
test_file_save_to_disk();
|
test_file_save_to_disk();
|
||||||
test_file_save_to_disk_ignore_existing();
|
|
||||||
test_file_save_to_disk_ignore_existing_entry_types();
|
|
||||||
test_file_write_to_disk_basic();
|
test_file_write_to_disk_basic();
|
||||||
test_file_write_to_disk_creates_dirs();
|
test_file_write_to_disk_creates_dirs();
|
||||||
test_file_write_to_disk_does_not_follow_symlink();
|
test_file_write_to_disk_does_not_follow_symlink();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "test_metadata.h"
|
#include "test_metadata.h"
|
||||||
|
#include "chmod.h"
|
||||||
#include "metadata.h"
|
#include "metadata.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
@@ -144,6 +145,27 @@ static void test_file_restore_metadata() {
|
|||||||
unlink(path);
|
unlink(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_chmod_changes() {
|
||||||
|
mode_t result;
|
||||||
|
EXPECT_TRUE(chmod_apply(0777, "u=rw,go=r", &result));
|
||||||
|
EXPECT_EQ_INT(result, 0644);
|
||||||
|
EXPECT_TRUE(chmod_apply(0644, "a+x", &result));
|
||||||
|
EXPECT_EQ_INT(result, 0755);
|
||||||
|
result = 0777;
|
||||||
|
EXPECT_TRUE(chmod_apply(0777, "0000", &result));
|
||||||
|
EXPECT_EQ_INT(result, 0000);
|
||||||
|
result = 0777;
|
||||||
|
EXPECT_TRUE(chmod_apply(0777, "7777", &result));
|
||||||
|
EXPECT_EQ_INT(result, 07777);
|
||||||
|
result = 0777;
|
||||||
|
EXPECT_TRUE(chmod_apply(0777, "755", &result));
|
||||||
|
EXPECT_EQ_INT(result, 0755);
|
||||||
|
EXPECT_FALSE(chmod_apply(0777, "888", &result));
|
||||||
|
EXPECT_FALSE(chmod_apply(0777, "10000", &result));
|
||||||
|
EXPECT_FALSE(chmod_apply(0777, "a+X", &result));
|
||||||
|
EXPECT_FALSE(chmod_apply(0777, "a+r,", &result));
|
||||||
|
}
|
||||||
|
|
||||||
void test_metadata() {
|
void test_metadata() {
|
||||||
test_metadata_to_from_buf_roundtrip();
|
test_metadata_to_from_buf_roundtrip();
|
||||||
test_metadata_to_buf_null();
|
test_metadata_to_buf_null();
|
||||||
@@ -152,4 +174,5 @@ void test_metadata() {
|
|||||||
test_metadata_send_null();
|
test_metadata_send_null();
|
||||||
test_metadata_rejects_invalid_values();
|
test_metadata_rejects_invalid_values();
|
||||||
test_file_restore_metadata();
|
test_file_restore_metadata();
|
||||||
|
test_chmod_changes();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user