Merge branch 'fix/w5-config' into fix/w5-integration
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
#include "client_validation.h"
|
||||
#include "charset.h"
|
||||
#include "delay_updates.h"
|
||||
#include "log.h"
|
||||
#include "usage.h"
|
||||
#include "utils.h"
|
||||
@@ -41,17 +39,6 @@ bool validate_config(const Config* config) {
|
||||
print_usage();
|
||||
return false;
|
||||
}
|
||||
if (config_has_basis(config) && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--compare-dest/--copy-dest/--link-dest require per-file incremental checks and "
|
||||
"cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
|
||||
log_message(LOG_LEVEL_ERROR, "-f/--sendfile cannot be combined with -c (compression) or -s "
|
||||
"(chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->compression_threads > 0 && !config->use_compression) {
|
||||
log_message(LOG_LEVEL_ERROR, "--compress-threads requires compression (-c or -z)");
|
||||
return false;
|
||||
@@ -60,73 +47,11 @@ bool validate_config(const Config* config) {
|
||||
log_message(LOG_LEVEL_ERROR, "-f/--sendfile is not supported with SSH transport");
|
||||
return false;
|
||||
}
|
||||
if (config->use_incremental && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR, "--incremental is not supported with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
/* -4 and -6 are mutually exclusive: a socket address family cannot be both. */
|
||||
if (config->ipv4 && config->ipv6) {
|
||||
log_message(LOG_LEVEL_ERROR, "-4/--ipv4 and -6/--ipv6 are mutually exclusive");
|
||||
return false;
|
||||
}
|
||||
if (config->skip_compress_set && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--skip-compress cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->use_delta && !config->whole_file && !config->use_incremental) {
|
||||
log_message(LOG_LEVEL_ERROR, "--delta requires --incremental");
|
||||
return false;
|
||||
}
|
||||
if (config->use_delta && !config->whole_file && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR, "--delta cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->use_delta && !config->whole_file && config->use_sendfile) {
|
||||
log_message(LOG_LEVEL_ERROR, "--delta cannot be combined with -f (sendfile)");
|
||||
return false;
|
||||
}
|
||||
/* --append / --append-verify resume a shorter existing destination by
|
||||
transmitting only the tail. The resume needs the per-file STATUS_CHECK
|
||||
handshake (so the dest length is learned), which chunk serialization -s
|
||||
disables; and whole-file is the opposite intent (send everything), so the
|
||||
two would silently make the resume pointless. Both are rejected up front
|
||||
rather than silently degrading to a full transfer. */
|
||||
if ((config->append || config->append_verify) && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--append/--append-verify require the per-file incremental check and cannot be "
|
||||
"combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if ((config->append || config->append_verify) && config->whole_file) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--append/--append-verify are incompatible with --whole-file (which forces a "
|
||||
"full transfer)");
|
||||
return false;
|
||||
}
|
||||
/* --hard-links/-H transmits each later group member as a dedicated per-file
|
||||
STATUS_HARDLINK frame, which chunk serialization -s does not support; and a
|
||||
hard-links sibling carries no payload, so the tail-resume of --append is
|
||||
meaningless for it. Both combinations are rejected up front rather than
|
||||
silently degrading. */
|
||||
if (config->preserve_hard_links && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--hard-links/-H cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
/* -X/-A ride the per-file metadata frame; the buffer-based chunk-serialization
|
||||
wire format does not carry the xattr block, so the pair is rejected up front
|
||||
(mirroring -H + -s) rather than silently dropping attributes. */
|
||||
if ((config->preserve_xattrs || config->preserve_acls) && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--xattrs/-X and --acls/-A cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->preserve_hard_links && (config->append || config->append_verify)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--hard-links/-H cannot be combined with --append/--append-verify");
|
||||
return false;
|
||||
}
|
||||
if (config->log_file_format && !config->log_file) {
|
||||
log_message(LOG_LEVEL_ERROR, "--log-file-format requires --log-file");
|
||||
return false;
|
||||
@@ -146,28 +71,12 @@ bool validate_config(const Config* config) {
|
||||
log_message(LOG_LEVEL_ERROR, "sending daemon credentials to a non-local server requires --tls");
|
||||
return false;
|
||||
}
|
||||
if (config->delay_updates && config->inplace) {
|
||||
log_message(LOG_LEVEL_ERROR, "--delay-updates does not work with --inplace");
|
||||
return false;
|
||||
}
|
||||
if (config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--backup-dir is reserved when --delay-updates is active (used for the internal "
|
||||
"staging directory)");
|
||||
return false;
|
||||
}
|
||||
if (!config_has_valid_delete_timing(config)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--delete-before/--delete-during/--delete-delay/--delete-after select the delete "
|
||||
"timing; at most one may be given and each implies --delete");
|
||||
return false;
|
||||
}
|
||||
/* --iconv: reject a malformed CONVERT_SPEC or an unsupported charset name at
|
||||
startup (a probe iconv_open is attempted), so a typo'd charset never fails
|
||||
the run mid-transfer with per-file errors. */
|
||||
if (!charset_spec_valid(config->iconv_spec)) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--iconv requires LOCAL[,REMOTE] charset names supported by iconv");
|
||||
/* Every cross-field invariant the receiver enforces lives in one shared
|
||||
predicate so the client and the server can never disagree. The client
|
||||
reports the specific reason here, before any network I/O. */
|
||||
const char* invariants_error = config_invariants_error(config);
|
||||
if (invariants_error) {
|
||||
log_message(LOG_LEVEL_ERROR, "%s", invariants_error);
|
||||
return false;
|
||||
}
|
||||
/* --protocol: FastSync has exactly one wire format, so the forced version
|
||||
@@ -180,15 +89,5 @@ bool validate_config(const Config* config) {
|
||||
PROTOCOL_VERSION);
|
||||
return false;
|
||||
}
|
||||
/* --copy-as pushes the source ids through the metadata path (it implies
|
||||
--preserve). A later --no-preserve would clear use_metadata, leaving the
|
||||
transfer with nothing to chown while the receiver gate would still pass.
|
||||
Refuse the combination up front rather than silently chowning nothing. */
|
||||
if (config->copy_as_set && !config->use_metadata) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--copy-as requires metadata preservation and cannot be combined with "
|
||||
"--no-preserve");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+55
-97
@@ -207,17 +207,20 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
char* data_pointer = data->data;
|
||||
size_t remaining_size = data->size;
|
||||
/* The element currently being parsed is owned by `files` only after the
|
||||
* array_list_add() at the end of the iteration; until then the error
|
||||
* epilogue destroys it directly. Keeping this one pointer nulled after the
|
||||
* hand-off makes the single cleanup path correct for every failure. */
|
||||
File* file = NULL;
|
||||
|
||||
while (remaining_size > 0) {
|
||||
if ((unsigned int)files->size >= MAX_FILES_PER_CHUNK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Chunk contains too many files");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
if (remaining_size < sizeof(size_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for path length");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
size_t path_len;
|
||||
@@ -227,26 +230,19 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
|
||||
if (path_len > SIZE_MAX - 1 || remaining_size < path_len) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for path");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (path_len == SIZE_MAX) {
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
char* path = protocol_alloc(path_len + 1);
|
||||
if (path == NULL) {
|
||||
log_perror("Could not allocate memory for file path");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
memcpy(path, data_pointer, path_len);
|
||||
path[path_len] = '\0';
|
||||
if (memchr(path, '\0', path_len) != NULL) {
|
||||
free(path);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
data_pointer += path_len;
|
||||
remaining_size -= path_len;
|
||||
@@ -260,8 +256,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
if (local_path == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--iconv: received chunk file name cannot be converted to the local charset");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
path = local_path;
|
||||
path_len = strlen(path);
|
||||
@@ -269,30 +264,23 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
|
||||
if (path_len == 0 || has_path_traversal(path)) {
|
||||
free(path);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
File* file = file_create(path);
|
||||
file = file_create(path);
|
||||
free(path);
|
||||
if (file == NULL) {
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
if (file == NULL)
|
||||
goto error;
|
||||
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
int entry_type;
|
||||
memcpy(&entry_type, data_pointer, sizeof(int));
|
||||
if (entry_type != 0 && entry_type != 1 && entry_type != 2 && entry_type != 3) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad entry type");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
file->is_dir = entry_type == 1;
|
||||
file->is_symlink = entry_type == 2;
|
||||
@@ -303,9 +291,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
if (file->is_special) {
|
||||
if (remaining_size < 2 * (int32_t)sizeof(int32_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for special rdev");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
int32_t special_major, special_minor;
|
||||
memcpy(&special_major, data_pointer, sizeof(special_major));
|
||||
@@ -320,9 +306,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
if (special_major < 0 || special_minor < 0 || special_major > 0xffff ||
|
||||
special_minor > 0x00ffffff) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: out-of-range special rdev");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
file->rdev_major = special_major;
|
||||
file->rdev_minor = special_minor;
|
||||
@@ -331,37 +315,32 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
if (use_metadata) {
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
// Peek at present flag to determine total size needed before reading
|
||||
/* Peek at the present flag to determine the total record size before
|
||||
decoding. metadata_from_buf() independently bounds-checks every read
|
||||
against remaining_size, so a short body can never over-read. */
|
||||
int present_flag;
|
||||
memcpy(&present_flag, data_pointer, sizeof(int));
|
||||
if ((present_flag != 0 && present_flag != 1) ||
|
||||
(present_flag == 1 && remaining_size < sizeof(int) + FILE_METADATA_WIRE_SIZE)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata body");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
file->metadata = metadata_from_buf(&data_pointer);
|
||||
remaining_size -= sizeof(int);
|
||||
file->metadata = metadata_from_buf((const uint8_t*)data_pointer, remaining_size);
|
||||
size_t metadata_consumed = sizeof(int);
|
||||
if (present_flag == 1) {
|
||||
if (file->metadata == NULL) {
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
remaining_size -= FILE_METADATA_WIRE_SIZE;
|
||||
if (file->metadata == NULL)
|
||||
goto error;
|
||||
metadata_consumed += FILE_METADATA_WIRE_SIZE;
|
||||
}
|
||||
data_pointer += metadata_consumed;
|
||||
remaining_size -= metadata_consumed;
|
||||
}
|
||||
|
||||
if (remaining_size < sizeof(size_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for data size");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
size_t file_data_size;
|
||||
@@ -371,35 +350,26 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
|
||||
if (remaining_size < file_data_size) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for file content");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
// Reject individual file data larger than the maximum allowed size.
|
||||
if (file_data_size > MAX_FILE_DATA_SIZE) {
|
||||
log_message(LOG_LEVEL_ERROR, "File data size %zu exceeds maximum %llu", file_data_size,
|
||||
(unsigned long long)MAX_FILE_DATA_SIZE);
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
|
||||
size_t allocation_size = file_data_size > 0 ? file_data_size : 1;
|
||||
void* file_data = protocol_alloc(allocation_size);
|
||||
if (file_data == NULL) {
|
||||
log_perror("Could not allocate memory for file data");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
memcpy(file_data, data_pointer, file_data_size);
|
||||
Data* replacement = data_create(file_data, file_data_size);
|
||||
if (replacement == NULL) {
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
if (replacement == NULL)
|
||||
goto error;
|
||||
data_destroy(file->data);
|
||||
file->data = replacement;
|
||||
data_pointer += file_data_size;
|
||||
@@ -408,9 +378,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
if (file->is_symlink) {
|
||||
if (remaining_size < sizeof(size_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for symlink target");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
size_t target_len;
|
||||
memcpy(&target_len, data_pointer, sizeof(size_t));
|
||||
@@ -418,24 +386,18 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
remaining_size -= sizeof(size_t);
|
||||
if (target_len == 0 || remaining_size < target_len) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: bad symlink target");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
char* target = protocol_alloc(target_len + 1);
|
||||
if (!target) {
|
||||
log_perror("Could not allocate memory for symlink target");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
memcpy(target, data_pointer, target_len);
|
||||
target[target_len] = '\0';
|
||||
if (memchr(target, '\0', target_len) != NULL) {
|
||||
free(target);
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
/* The symlink target also rides the wire charset; decode it to the local
|
||||
charset like the path (a target is a path). */
|
||||
@@ -446,9 +408,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--iconv: received chunk symlink target cannot be converted to the local "
|
||||
"charset");
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
goto error;
|
||||
}
|
||||
target = local_target;
|
||||
}
|
||||
@@ -457,29 +417,27 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
remaining_size -= target_len;
|
||||
}
|
||||
|
||||
if (!array_list_add(files, file)) {
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
if (!array_list_add(files, file))
|
||||
goto error;
|
||||
file = NULL;
|
||||
}
|
||||
|
||||
File** file_array = (File**)array_list_to_array(files);
|
||||
if (files->size > 0 && file_array == NULL) {
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
if (files->size > 0 && file_array == NULL)
|
||||
goto error;
|
||||
Chunk* chunk = chunk_create(file_array, files->size);
|
||||
|
||||
free(file_array);
|
||||
if (chunk == NULL) {
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
if (chunk == NULL)
|
||||
goto error;
|
||||
files->item_destroyer = NULL;
|
||||
array_list_delete(files);
|
||||
|
||||
return chunk;
|
||||
|
||||
error:
|
||||
if (file)
|
||||
file_destroy(file);
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* chunk_compress(Chunk* chunk, int compression_level, bool use_metadata) {
|
||||
|
||||
+65
-25
@@ -252,6 +252,11 @@ static char* config_receive_str_redacted(int fd, ConfigStringBudget* budget) {
|
||||
}
|
||||
|
||||
static bool validate_received_config(const Config* config) {
|
||||
/* Cross-field invariants live in one place (config_invariants_error) so the
|
||||
receiver enforces every combination the client relies on; a hostile peer
|
||||
can forge a frame that violates any clause of the shared predicate. */
|
||||
if (config_invariants_error(config) != NULL)
|
||||
return false;
|
||||
return valid_wire_bool(config->save_to_disk) && valid_wire_bool(config->use_multithreading) &&
|
||||
valid_wire_bool(config->use_chunk_serialization) &&
|
||||
valid_wire_bool(config->use_compression) && valid_wire_bool(config->use_metadata) &&
|
||||
@@ -275,30 +280,14 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->delete_delay) && valid_wire_bool(config->delete_during) &&
|
||||
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||
valid_wire_bool(config->delay_updates) && valid_wire_bool(config->mkpath) &&
|
||||
!(config->delay_updates && config->inplace) &&
|
||||
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
||||
checksum_algo_valid(config->checksum_algo) && config_has_valid_delete_timing(config) &&
|
||||
identity_wire_valid(config) &&
|
||||
!(config->skip_compress_set && config->use_chunk_serialization) &&
|
||||
/* --append / --append-verify tail resume needs the per-file check,
|
||||
which chunk serialization -s disables: reject on the receiver too
|
||||
so a -s sender cannot negotiate an inert append mode. */
|
||||
!((config->append || config->append_verify) && config->use_chunk_serialization) &&
|
||||
!(config->preserve_hard_links && config->use_chunk_serialization) &&
|
||||
!(config->preserve_hard_links && (config->append || config->append_verify)) &&
|
||||
/* The xattr block rides the per-file streaming frame, which -s drops. */
|
||||
!((config->preserve_xattrs || config->preserve_acls) && config->use_chunk_serialization) &&
|
||||
checksum_algo_valid(config->checksum_algo) && identity_wire_valid(config) &&
|
||||
valid_wire_bool(config->preserve_atimes) && valid_wire_bool(config->preserve_crtimes) &&
|
||||
valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->omit_link_times) &&
|
||||
valid_wire_bool(config->munge_links) && valid_wire_bool(config->keep_dirlinks) &&
|
||||
valid_wire_bool(config->fake_super) &&
|
||||
(!config->copy_as_set || (config->copy_as_uid >= 0 && config->copy_as_gid >= 0)) &&
|
||||
/* --copy-as forces ownership through the metadata path; without
|
||||
metadata it would pass the privilege gate but silently chown
|
||||
nothing. Refuse the frame instead. */
|
||||
(!config->copy_as_set || config->use_metadata) &&
|
||||
(!config->use_compression ||
|
||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||
@@ -309,14 +298,6 @@ static bool validate_received_config(const Config* config) {
|
||||
config->skip_compress_count <= MAX_SKIP_COMPRESS_SUFFIXES && config->max_alloc > 0 &&
|
||||
(!config->chmod_spec || !*config->chmod_spec ||
|
||||
chmod_apply(0, config->chmod_spec, &(mode_t){0})) &&
|
||||
/* The received --iconv CONVERT_SPEC is untrusted input that drives
|
||||
the receiver's path decoding: reject a malformed spec or an
|
||||
unsupported charset name so the run is refused up front instead of
|
||||
every received file name failing mid-transfer. A NULL spec (iconv
|
||||
disabled) is always accepted. */
|
||||
(!config->iconv_spec || charset_spec_valid(config->iconv_spec)) &&
|
||||
/* --super / --no-super: the received tri-state must be one of the
|
||||
defined values (AUTO/ON/OFF); anything else is a malformed frame. */
|
||||
config->super_mode >= SUPER_MODE_AUTO && config->super_mode <= SUPER_MODE_OFF;
|
||||
}
|
||||
|
||||
@@ -348,6 +329,65 @@ bool config_has_valid_delete_timing(const Config* config) {
|
||||
return timing_count <= 1;
|
||||
}
|
||||
|
||||
/* The cross-field invariants FastSync relies on, in one place. Every message
|
||||
* here was previously duplicated (verbatim) in client_validation.c and/or
|
||||
* config.c; the client reports the returned string for UX and the server
|
||||
* enforces the same rules at its trust boundary. Pure: no I/O, no logging.
|
||||
* The order is deliberate (most specific structural conflicts first). */
|
||||
const char* config_invariants_error(const Config* config) {
|
||||
if (!config)
|
||||
return "Invalid configuration";
|
||||
if (config_has_basis(config) && config->use_chunk_serialization)
|
||||
return "--compare-dest/--copy-dest/--link-dest require per-file incremental checks and cannot "
|
||||
"be combined with -s (chunk serialization)";
|
||||
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression))
|
||||
return "-f/--sendfile cannot be combined with -c (compression) or -s (chunk serialization)";
|
||||
if (config->use_incremental && config->use_chunk_serialization)
|
||||
return "--incremental is not supported with -s (chunk serialization)";
|
||||
if (config->skip_compress_set && config->use_chunk_serialization)
|
||||
return "--skip-compress cannot be combined with -s (chunk serialization)";
|
||||
if (config->use_delta && !config->whole_file && !config->use_incremental)
|
||||
return "--delta requires --incremental";
|
||||
if (config->use_delta && !config->whole_file && config->use_chunk_serialization)
|
||||
return "--delta cannot be combined with -s (chunk serialization)";
|
||||
if (config->use_delta && !config->whole_file && config->use_sendfile)
|
||||
return "--delta cannot be combined with -f (sendfile)";
|
||||
/* --append / --append-verify resume a shorter existing destination by
|
||||
transmitting only the tail. The resume needs the per-file STATUS_CHECK
|
||||
handshake (so the dest length is learned), which chunk serialization -s
|
||||
disables; whole-file is the opposite intent (send everything). */
|
||||
if ((config->append || config->append_verify) && config->use_chunk_serialization)
|
||||
return "--append/--append-verify require the per-file incremental check and cannot be "
|
||||
"combined with -s (chunk serialization)";
|
||||
if ((config->append || config->append_verify) && config->whole_file)
|
||||
return "--append/--append-verify are incompatible with --whole-file (which forces a full "
|
||||
"transfer)";
|
||||
/* -H transmits each later hard-link group member as a dedicated per-file
|
||||
STATUS_HARDLINK frame, which -s does not support; and a hard-links sibling
|
||||
carries no payload, so the tail-resume of --append is meaningless. */
|
||||
if (config->preserve_hard_links && config->use_chunk_serialization)
|
||||
return "--hard-links/-H cannot be combined with -s (chunk serialization)";
|
||||
/* -X/-A ride the per-file metadata frame; the chunk-serialization wire format
|
||||
does not carry the xattr block. */
|
||||
if ((config->preserve_xattrs || config->preserve_acls) && config->use_chunk_serialization)
|
||||
return "--xattrs/-X and --acls/-A cannot be combined with -s (chunk serialization)";
|
||||
if (config->preserve_hard_links && (config->append || config->append_verify))
|
||||
return "--hard-links/-H cannot be combined with --append/--append-verify";
|
||||
if (config->delay_updates && config->inplace)
|
||||
return "--delay-updates does not work with --inplace";
|
||||
if (config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir))
|
||||
return "--backup-dir is reserved when --delay-updates is active (used for the internal "
|
||||
"staging directory)";
|
||||
if (!config_has_valid_delete_timing(config))
|
||||
return "--delete-before/--delete-during/--delete-delay/--delete-after select the delete "
|
||||
"timing; at most one may be given and each implies --delete";
|
||||
if (config->iconv_spec && !charset_spec_valid(config->iconv_spec))
|
||||
return "--iconv requires LOCAL[,REMOTE] charset names supported by iconv";
|
||||
if (config->copy_as_set && !config->use_metadata)
|
||||
return "--copy-as requires metadata preservation and cannot be combined with --no-preserve";
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool config_has_basis(const Config* config) {
|
||||
return config && config->basis_count > 0;
|
||||
}
|
||||
|
||||
@@ -754,6 +754,16 @@ bool config_delete_timing_early(const Config* config);
|
||||
* set (none = the default delete-after commit timing); without deletion no
|
||||
* timing flag may be set (each timing flag implies --delete). */
|
||||
bool config_has_valid_delete_timing(const Config* config);
|
||||
|
||||
/* Single source of truth for the cross-field ("combination") invariants a
|
||||
* Config must satisfy. Returns NULL when `config` is consistent, or a static,
|
||||
* human-readable error string (no trailing period) describing the FIRST
|
||||
* violation found. Pure: performs no I/O, no allocation, no logging and no
|
||||
* printing, so it is safe to call from every trust boundary. The client calls
|
||||
* it from validate_config() for up-front UX and the server calls it from
|
||||
* validate_received_config() so the receiver enforces exactly the same
|
||||
* invariants it relies on (the server is the trust boundary). */
|
||||
const char* config_invariants_error(const Config* config);
|
||||
/* True when at least one --compare-dest/--copy-dest/--link-dest was set. */
|
||||
bool config_has_basis(const Config* config);
|
||||
/* Append one basis-dir entry. Returns 0 on success, -1 on allocation failure. */
|
||||
|
||||
+32
-31
@@ -90,63 +90,65 @@ void metadata_to_buf(char** buf, const FileMetadata* m) {
|
||||
*buf += sizeof(crtime_nsec);
|
||||
}
|
||||
|
||||
FileMetadata* metadata_from_buf(char** buf) {
|
||||
FileMetadata* metadata_from_buf(const uint8_t* buf, size_t len) {
|
||||
if (buf == NULL || len < sizeof(int32_t))
|
||||
return NULL;
|
||||
int32_t present;
|
||||
memcpy(&present, *buf, sizeof(present));
|
||||
*buf += sizeof(present);
|
||||
if (present != 0 && present != 1)
|
||||
memcpy(&present, buf, sizeof(present));
|
||||
if (present != 1)
|
||||
return NULL;
|
||||
if (!present)
|
||||
if (len < sizeof(int32_t) + FILE_METADATA_WIRE_SIZE)
|
||||
return NULL;
|
||||
const uint8_t* cursor = buf + sizeof(int32_t);
|
||||
FileMetadata* m = protocol_alloc(sizeof(FileMetadata));
|
||||
if (m == NULL)
|
||||
return NULL;
|
||||
int32_t mode;
|
||||
memcpy(&mode, *buf, sizeof(mode));
|
||||
*buf += sizeof(mode);
|
||||
memcpy(&mode, cursor, sizeof(mode));
|
||||
cursor += sizeof(mode);
|
||||
m->mode = (mode_t)mode;
|
||||
int32_t uid;
|
||||
memcpy(&uid, *buf, sizeof(uid));
|
||||
*buf += sizeof(uid);
|
||||
memcpy(&uid, cursor, sizeof(uid));
|
||||
cursor += sizeof(uid);
|
||||
m->uid = (uid_t)uid;
|
||||
int32_t gid;
|
||||
memcpy(&gid, *buf, sizeof(gid));
|
||||
*buf += sizeof(gid);
|
||||
memcpy(&gid, cursor, sizeof(gid));
|
||||
cursor += sizeof(gid);
|
||||
m->gid = (gid_t)gid;
|
||||
int64_t mtime_sec;
|
||||
memcpy(&mtime_sec, *buf, sizeof(mtime_sec));
|
||||
*buf += sizeof(mtime_sec);
|
||||
memcpy(&mtime_sec, cursor, sizeof(mtime_sec));
|
||||
cursor += sizeof(mtime_sec);
|
||||
m->mtime_sec = (time_t)mtime_sec;
|
||||
int64_t mtime_nsec;
|
||||
memcpy(&mtime_nsec, *buf, sizeof(mtime_nsec));
|
||||
*buf += sizeof(mtime_nsec);
|
||||
memcpy(&mtime_nsec, cursor, sizeof(mtime_nsec));
|
||||
cursor += sizeof(mtime_nsec);
|
||||
m->mtime_nsec = (long)mtime_nsec;
|
||||
int32_t atime_valid;
|
||||
memcpy(&atime_valid, *buf, sizeof(atime_valid));
|
||||
*buf += sizeof(atime_valid);
|
||||
memcpy(&atime_valid, cursor, sizeof(atime_valid));
|
||||
cursor += sizeof(atime_valid);
|
||||
int64_t atime_sec;
|
||||
memcpy(&atime_sec, *buf, sizeof(atime_sec));
|
||||
*buf += sizeof(atime_sec);
|
||||
memcpy(&atime_sec, cursor, sizeof(atime_sec));
|
||||
cursor += sizeof(atime_sec);
|
||||
int64_t atime_nsec;
|
||||
memcpy(&atime_nsec, *buf, sizeof(atime_nsec));
|
||||
*buf += sizeof(atime_nsec);
|
||||
memcpy(&atime_nsec, cursor, sizeof(atime_nsec));
|
||||
cursor += sizeof(atime_nsec);
|
||||
int32_t crtime_valid;
|
||||
memcpy(&crtime_valid, *buf, sizeof(crtime_valid));
|
||||
*buf += sizeof(crtime_valid);
|
||||
memcpy(&crtime_valid, cursor, sizeof(crtime_valid));
|
||||
cursor += sizeof(crtime_valid);
|
||||
int64_t crtime_sec;
|
||||
memcpy(&crtime_sec, *buf, sizeof(crtime_sec));
|
||||
*buf += sizeof(crtime_sec);
|
||||
memcpy(&crtime_sec, cursor, sizeof(crtime_sec));
|
||||
cursor += sizeof(crtime_sec);
|
||||
int64_t crtime_nsec;
|
||||
memcpy(&crtime_nsec, *buf, sizeof(crtime_nsec));
|
||||
*buf += sizeof(crtime_nsec);
|
||||
memcpy(&crtime_nsec, cursor, sizeof(crtime_nsec));
|
||||
cursor += sizeof(crtime_nsec);
|
||||
m->atime_valid = atime_valid != 0;
|
||||
m->atime_sec = (time_t)atime_sec;
|
||||
m->atime_nsec = (long)atime_nsec;
|
||||
m->crtime_valid = crtime_valid != 0;
|
||||
m->crtime_sec = (time_t)crtime_sec;
|
||||
m->crtime_nsec = (long)crtime_nsec;
|
||||
if (present != 1 || mtime_nsec < 0 || mtime_nsec >= 1000000000LL || mode < 0 || uid < 0 ||
|
||||
gid < 0 || atime_valid < 0 || atime_valid > 1 || crtime_valid < 0 || crtime_valid > 1 ||
|
||||
if (mtime_nsec < 0 || mtime_nsec >= 1000000000LL || mode < 0 || uid < 0 || gid < 0 ||
|
||||
atime_valid < 0 || atime_valid > 1 || crtime_valid < 0 || crtime_valid > 1 ||
|
||||
(atime_valid && (atime_nsec < 0 || atime_nsec >= 1000000000LL)) ||
|
||||
(crtime_valid && (crtime_nsec < 0 || crtime_nsec >= 1000000000LL))) {
|
||||
free(m);
|
||||
@@ -196,8 +198,7 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
|
||||
*ok = 0;
|
||||
return NULL;
|
||||
}
|
||||
char* cursor = packed;
|
||||
FileMetadata* m = metadata_from_buf(&cursor);
|
||||
FileMetadata* m = metadata_from_buf((const uint8_t*)packed, sizeof(packed));
|
||||
if (m == NULL) {
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "file.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
@@ -39,7 +40,13 @@
|
||||
#define FILE_METADATA_WIRE_SIZE (sizeof(int32_t) * 5 + sizeof(int64_t) * 6)
|
||||
|
||||
void metadata_to_buf(char** buf, const FileMetadata* m);
|
||||
FileMetadata* metadata_from_buf(char** buf);
|
||||
/* Decode one packed metadata record (an int32 present flag followed, when
|
||||
* present, by FILE_METADATA_WIRE_SIZE field bytes) from `buf`, which has `len`
|
||||
* readable bytes. Every read is bounds-checked against `len`, so the function
|
||||
* can never over-read the caller's buffer: a too-short record, an absent
|
||||
* (present == 0) record and a malformed record all return NULL. A successful
|
||||
* decode returns a heap-allocated FileMetadata owned by the caller. */
|
||||
FileMetadata* metadata_from_buf(const uint8_t* buf, size_t len);
|
||||
bool metadata_send(int file_descriptor, const FileMetadata* m);
|
||||
FileMetadata* metadata_receive(int file_descriptor, int* ok);
|
||||
void file_restore_metadata(const char* path, const FileMetadata* metadata,
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < sizeof(int) + FILE_METADATA_WIRE_SIZE)
|
||||
return 0;
|
||||
|
||||
char* buf = malloc(size);
|
||||
/* Exercise the bounds-checked decoder on EVERY input length, including
|
||||
* records shorter than a full metadata body; the decoder must reject those
|
||||
* without reading past `size`. */
|
||||
char* buf = malloc(size > 0 ? size : 1);
|
||||
if (!buf)
|
||||
return 0;
|
||||
if (size > 0)
|
||||
memcpy(buf, data, size);
|
||||
|
||||
char* original_buf = buf;
|
||||
FileMetadata* m = metadata_from_buf(&buf);
|
||||
FileMetadata* m = metadata_from_buf((const uint8_t*)buf, size);
|
||||
if (m)
|
||||
free(m);
|
||||
|
||||
free(original_buf);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,52 @@ static void test_validate_config_delta_sendfile_constraints() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* The client must still reject every combination now enforced by the shared
|
||||
config_invariants_error() predicate (the server trusts the same rules). */
|
||||
static void test_validate_config_unified_invariants() {
|
||||
Config* cfg = valid_client_config();
|
||||
cfg->use_incremental = true;
|
||||
cfg->use_delta = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* delta + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->use_delta = true; /* whole_file false */
|
||||
EXPECT_FALSE(validate_config(cfg)); /* delta without incremental */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->use_sendfile = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* sendfile + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_hard_links = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* hard-links + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_hard_links = true;
|
||||
cfg->append = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* hard-links + append */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->append = true;
|
||||
cfg->whole_file = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* append + whole-file */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_xattrs = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* xattrs + chunk */
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test main() with --help flag (early return path, no server connection needed) */
|
||||
static void test_cli_help() {
|
||||
/* We can't easily call main() because it calls send_files which needs a server.
|
||||
@@ -3155,6 +3201,7 @@ void test_client_cli() {
|
||||
test_validate_config_tls_requirements();
|
||||
test_validate_config_credentials_require_tls_or_loopback();
|
||||
test_validate_config_delta_sendfile_constraints();
|
||||
test_validate_config_unified_invariants();
|
||||
test_cli_help();
|
||||
test_cli_archive_flags();
|
||||
test_cli_dry_run();
|
||||
|
||||
@@ -2042,6 +2042,156 @@ static void test_super_does_not_imply_numeric() {
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
/* The single shared predicate must reject every cross-field combination the
|
||||
client/server enforce and accept a plain valid config. Because both
|
||||
validate_config() (client) and validate_received_config() (server) call it,
|
||||
this table documents the whole invariant set in one place. */
|
||||
static void test_config_invariants_error_all_combinations() {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->send_directory = str_dup("/src");
|
||||
c->receive_root_directory = str_dup("/dst");
|
||||
EXPECT_NULL(config_invariants_error(c));
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_COMPARE, "sub"), 0);
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* basis + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_sendfile = true;
|
||||
c->use_compression = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + compression */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_sendfile = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_incremental = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* incremental + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->skip_compress_set = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* skip-compress + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true; /* whole_file false -> active */
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta without incremental */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true;
|
||||
c->use_incremental = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true;
|
||||
c->use_incremental = true;
|
||||
c->use_sendfile = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + sendfile */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->append = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->append = true;
|
||||
c->whole_file = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + whole-file */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_hard_links = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_xattrs = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* xattrs + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_hard_links = true;
|
||||
c->append = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + append */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delay_updates = true;
|
||||
c->inplace = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates + inplace */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delay_updates = true;
|
||||
c->backup_dir = str_dup(".fastsync-stage");
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates staging conflict */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delete_delay = true; /* a timing flag without --delete */
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* invalid delete timing */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->iconv_spec = str_dup("no-such-charset,utf-8");
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* malformed iconv spec */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->copy_as_set = true;
|
||||
c->use_metadata = false;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* copy-as without metadata */
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
/* The receiver previously missed several of these; a forged frame that sets
|
||||
the offending serialized fields must now be refused at the config
|
||||
handshake. (whole_file is client-only, so its rules cannot appear here.) */
|
||||
static void test_config_receive_rejects_unified_invariants() {
|
||||
if (is_running_under_valgrind())
|
||||
return;
|
||||
struct {
|
||||
bool incremental, delta, chunk, sendfile, compression;
|
||||
} cases[] = {
|
||||
{true, false, true, false, false}, /* --incremental + -s */
|
||||
{false, true, true, false, false}, /* --delta + -s */
|
||||
{false, true, false, false, false}, /* --delta without --incremental */
|
||||
{false, false, false, true, true}, /* --sendfile + compression */
|
||||
{false, false, true, true, false}, /* --sendfile + -s */
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->send_directory = str_dup("/src");
|
||||
c->receive_root_directory = str_dup("/dst");
|
||||
c->use_incremental = cases[i].incremental;
|
||||
c->use_delta = cases[i].delta;
|
||||
c->use_chunk_serialization = cases[i].chunk;
|
||||
c->use_sendfile = cases[i].sendfile;
|
||||
c->use_compression = cases[i].compression;
|
||||
EXPECT_FALSE(roundtrip_config_ok(c));
|
||||
config_delete(c);
|
||||
}
|
||||
}
|
||||
|
||||
void test_config() {
|
||||
test_config_lifecycle();
|
||||
test_config_ssh_dest();
|
||||
@@ -2095,6 +2245,8 @@ void test_config() {
|
||||
test_config_receive_rejects_copy_as_without_metadata();
|
||||
test_config_receive_rejects_oversized_string_budget();
|
||||
test_config_receive_with_validate_rejects();
|
||||
test_config_invariants_error_all_combinations();
|
||||
test_config_receive_rejects_unified_invariants();
|
||||
}
|
||||
test_identity_copy_as_refused();
|
||||
test_identity_ownership_requested();
|
||||
|
||||
@@ -128,8 +128,7 @@ static void test_fuzz_metadata_from_buf() {
|
||||
EXPECT_EQ_INT((int)(meta_ptr - meta_buf), (int)meta_buf_size);
|
||||
|
||||
/* Deserialize from buffer (simulates fuzz_metadata_from_buf) */
|
||||
char* buf_copy = meta_buf;
|
||||
FileMetadata* deserialized = metadata_from_buf(&buf_copy);
|
||||
FileMetadata* deserialized = metadata_from_buf((const uint8_t*)meta_buf, (size_t)meta_buf_size);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT((int)deserialized->mode, (int)meta->mode);
|
||||
EXPECT_EQ_INT((int)deserialized->mtime_sec, (int)meta->mtime_sec);
|
||||
|
||||
+38
-9
@@ -29,8 +29,8 @@ static void test_metadata_to_from_buf_roundtrip() {
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, &original);
|
||||
|
||||
char* read_ptr = buf;
|
||||
FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
FileMetadata* result =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
|
||||
EXPECT_NOT_NULL(result);
|
||||
EXPECT_EQ_INT(result->mode, 0755);
|
||||
@@ -45,8 +45,6 @@ static void test_metadata_to_from_buf_roundtrip() {
|
||||
EXPECT_EQ_INT(result->crtime_sec, 1200000000);
|
||||
EXPECT_EQ_INT(result->crtime_nsec, 750000000);
|
||||
|
||||
EXPECT_EQ_INT((int)(read_ptr - buf), (int)FILE_METADATA_WIRE_SIZE + (int)sizeof(int));
|
||||
|
||||
free(result);
|
||||
free(buf);
|
||||
}
|
||||
@@ -71,14 +69,46 @@ static void test_metadata_from_buf_null() {
|
||||
int present = 0;
|
||||
memcpy(buf, &present, sizeof(int));
|
||||
|
||||
char* read_ptr = buf;
|
||||
const FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
const FileMetadata* result =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
|
||||
EXPECT_NULL(result);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* The decoder must reject (never over-read) a present record that is even one
|
||||
* byte shorter than the full int32 flag + FILE_METADATA_WIRE_SIZE body, and
|
||||
* must reject a buffer too short to even hold the present flag. */
|
||||
static void test_metadata_from_buf_bounds() {
|
||||
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
FileMetadata original = {.mode = 0644,
|
||||
.uid = 1,
|
||||
.gid = 2,
|
||||
.mtime_sec = 3,
|
||||
.mtime_nsec = 4,
|
||||
.atime_valid = true,
|
||||
.atime_sec = 5,
|
||||
.atime_nsec = 6,
|
||||
.crtime_valid = false};
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, &original);
|
||||
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, 0));
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, sizeof(int)));
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int) - 1));
|
||||
/* A buffer larger than the record decodes using only the record prefix. */
|
||||
FileMetadata* decoded =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int) + 16);
|
||||
EXPECT_NOT_NULL(decoded);
|
||||
EXPECT_EQ_INT(decoded->mode, 0644);
|
||||
free(decoded);
|
||||
EXPECT_NULL(metadata_from_buf(NULL, FILE_METADATA_WIRE_SIZE + sizeof(int)));
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_send_receive_roundtrip() {
|
||||
io_set_bwlimit(0);
|
||||
int p[2];
|
||||
@@ -169,10 +199,8 @@ static void test_metadata_wire_is_one_packed_frame() {
|
||||
EXPECT_EQ_INT(avail, 0);
|
||||
|
||||
/* The present frame decodes in one shot with the shared codec. */
|
||||
char* cursor = (char*)wire;
|
||||
FileMetadata* decoded = metadata_from_buf(&cursor);
|
||||
FileMetadata* decoded = metadata_from_buf((const uint8_t*)wire, sizeof(wire));
|
||||
EXPECT_NOT_NULL(decoded);
|
||||
EXPECT_EQ_INT((int)(cursor - (char*)wire), (int)sizeof(wire));
|
||||
EXPECT_EQ_INT(decoded->mode, 0640);
|
||||
EXPECT_EQ_INT(decoded->uid, 42);
|
||||
EXPECT_EQ_INT(decoded->gid, 43);
|
||||
@@ -451,6 +479,7 @@ void test_metadata() {
|
||||
test_metadata_to_from_buf_roundtrip();
|
||||
test_metadata_to_buf_null();
|
||||
test_metadata_from_buf_null();
|
||||
test_metadata_from_buf_bounds();
|
||||
test_metadata_send_receive_roundtrip();
|
||||
test_metadata_send_null();
|
||||
test_metadata_wire_is_one_packed_frame();
|
||||
|
||||
Reference in New Issue
Block a user