Merge remote-tracking branch 'origin/feat/chmod' into dev

This commit is contained in:
2026-09-04 18:20:08 +02:00
parent 315e572d18
commit 4f21498ee6
11 changed files with 229 additions and 10 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `-g`, `--group` | Preserve group | ✅ Implemented | Part of -M |
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
| `-E`, `--executability` | Preserve executability | ✅ Implemented | Preserves executable permission bits (implies metadata preservation) |
| `--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 |
| `-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 |
+22
View File
@@ -1,5 +1,6 @@
#include "client_send.h"
#include "client_validation.h"
#include "chmod.h"
#include "config.h"
#include "delta.h"
#include "log.h"
@@ -231,6 +232,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--8-bit-output", "-8", OPT_FLAG, offsetof(Config, eight_bit_output)},
{"--existing", NULL, OPT_FLAG, offsetof(Config, existing)},
{"--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)},
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
@@ -319,6 +321,14 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
}
if (apply_table_option(config, entry, argv[++i]) != 0)
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) {
return -1;
}
@@ -327,6 +337,18 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
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)) {
print_usage();
return 1;
+1
View File
@@ -51,6 +51,7 @@ void print_usage(void) {
printf(" --debug=FLAGS Fine-grained debug logging (use --debug=help for flags)\n");
printf(" -M, --preserve Preserve file metadata\n");
printf(" -E, --executability Preserve executable permission bits\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(" --source-dir <path> Source directory\n");
printf(" --dest-dir <path> Destination directory\n");
+90
View File
@@ -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;
}
+10
View File
@@ -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
+12 -3
View File
@@ -1,4 +1,5 @@
#include "config.h"
#include "chmod.h"
#include "delta.h"
#include "log.h"
#include "protocol.h"
@@ -108,6 +109,7 @@ static void config_set_defaults(Config* config) {
config->server_mode = false;
config->checksum = false;
config->compress_choice = NULL;
config->chmod_spec = NULL;
}
static bool valid_wire_bool(int value) {
@@ -149,7 +151,9 @@ static bool validate_received_config(const Config* config) {
config->delta_block_size >= DELTA_BLOCK_SIZE_MIN &&
config->delta_block_size <= DELTA_BLOCK_SIZE_MAX &&
config->delta_max_file_size <= DELTA_MAX_FILE_SIZE && config->modify_window >= 0 &&
config->max_delete >= 0;
config->max_delete >= 0 &&
(!config->chmod_spec || !*config->chmod_spec ||
chmod_apply(0, config->chmod_spec, &(mode_t){0}));
}
Config* config_create(void) {
@@ -223,6 +227,7 @@ void config_delete(Config* config) {
free(config->bind_address);
free(config->daemon_config);
free(config->compress_choice);
free(config->chmod_spec);
if (config->filters) {
array_list_delete(config->filters);
}
@@ -275,7 +280,8 @@ static bool send_resume_options(int fd, const Config* c) {
send_str(fd, c->partial_dir ? c->partial_dir : "") &&
send_str(fd, c->suffix ? c->suffix : "") && send_int(fd, c->delete_before) &&
send_int(fd, c->checksum) && send_int(fd, c->modify_window) &&
send_str(fd, c->compress_choice ? c->compress_choice : "");
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) {
@@ -363,7 +369,10 @@ static bool receive_resume_options(int fd, Config* c) {
if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window)))
return false;
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) {
+1
View File
@@ -136,6 +136,7 @@ typedef struct Config {
// PR #184: Compression algorithm negotiation
char* compress_choice;
char* chmod_spec;
} Config;
#define PROTOCOL_VERSION "2.3.0"
+15 -6
View File
@@ -8,6 +8,7 @@
#include <unistd.h>
#include "array_list.h"
#include "chmod.h"
#include "compression.h"
#include "config.h"
#include "data.h"
@@ -131,17 +132,25 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
}
}
FileMetadata adjusted_metadata;
const FileMetadata* metadata = file->metadata;
if (metadata && config && config->chmod_spec && *config->chmod_spec) {
adjusted_metadata = *metadata;
if (!chmod_apply(adjusted_metadata.mode, config->chmod_spec, &adjusted_metadata.mode))
goto fail;
metadata = &adjusted_metadata;
}
bool ok = config && config->ignore_existing
? file_to_disk_secure_no_replace(disk_path, file->data->data, file->data->size,
sparse, file->metadata, preserve_executability)
sparse, metadata, preserve_executability)
: config && config->update
? file_to_disk_secure_update(disk_path, file->data->data, file->data->size,
inplace, sparse, file->metadata,
preserve_executability)
inplace, sparse, metadata,
preserve_executability)
: file_to_disk_secure_with_fsync(disk_path, file->data->data, file->data->size,
inplace, sparse, file->metadata,
preserve_executability,
config && config->use_fsync);
inplace, sparse, metadata,
preserve_executability,
config && config->use_fsync);
free(parent_copy);
free(backup_path);
free(confined_backup);
+16
View File
@@ -190,6 +190,22 @@ class TestExecutability:
assert received_mode & 0o077 == 0o011
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:
def test_exclude_single(self, shared_server):
clean_dir(DEST_DIR)
+38
View File
@@ -1,5 +1,6 @@
#include "test_client_cli.h"
#include "client_validation.h"
#include "chmod.h"
#include "config.h"
#include "log.h"
#include "test_utils.h"
@@ -250,6 +251,40 @@ static void test_parse_args_executability() {
config_delete(cfg);
}
static void test_parse_args_chmod() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--chmod=u=rw,go=r", "/src", "/dst"};
int positional_args[2];
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);
}
static void test_parse_args_numeric_chmod() {
Config* cfg = config_create();
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);
}
/* Test parse_args rejects port > 65535 */
static void test_parse_args_invalid_port() {
Config* cfg = config_create();
@@ -671,6 +706,9 @@ void test_client_cli() {
test_parse_args_size_only();
test_parse_args_ignore_existing();
test_parse_args_executability();
test_parse_args_chmod();
test_parse_args_numeric_chmod();
test_parse_args_rejects_invalid_chmod();
test_parse_args_invalid_port();
test_parse_args_non_numeric_port();
test_parse_args_invalid_server_port();
+23
View File
@@ -1,4 +1,5 @@
#include "test_metadata.h"
#include "chmod.h"
#include "metadata.h"
#include "protocol.h"
#include "test_utils.h"
@@ -185,6 +186,27 @@ static void test_directory_restore_executability_only() {
rmdir(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() {
test_metadata_to_from_buf_roundtrip();
test_metadata_to_buf_null();
@@ -196,4 +218,5 @@ void test_metadata() {
test_file_restore_metadata();
test_file_restore_executability_only();
test_directory_restore_executability_only();
test_chmod_changes();
}