10 Commits
Author SHA1 Message Date
TapTap a2ac599298 docs: recount RSYNC_COMPAT to 121/10 after Phase 6 waves A-B (--stop-after/--stop-at, --iconv)
CI / lint (push) Successful in 1m15s
CI / sanitizers (address) (push) Successful in 53s
CI / sanitizers (undefined) (push) Successful in 52s
CI / fuzz-build (push) Successful in 22s
CI / coverage (push) Successful in 45s
CI / valgrind (push) Successful in 38s
CI / build-and-test (push) Successful in 4m40s
2026-09-10 18:18:17 +02:00
TapTap 282aebb7f5 Merge feat/p6-stop: --stop-after/--stop-at deadline stop 2026-09-10 18:16:55 +02:00
TapTap b2afcf2c65 Merge feat/p6-iconv: --iconv charset conversion + PROTOCOL 2.16.0 2026-09-10 18:16:51 +02:00
TapTap 09a07179c9 fix(p6-stop): init -m scan_stopped_early; gate delete warning; stabilize partial-stop test 2026-09-10 18:16:35 +02:00
TapTap cac805b661 fix(p6-iconv): prevent convert buffer overflow; validate both directions; reset on error; more tests 2026-09-10 17:49:29 +02:00
TapTap ac7e9e3bc1 fix(p6-stop): block delete-manifest on early stop; sync -m manifest access; overflow guard 2026-09-10 17:39:34 +02:00
TapTap 7d6665633d test(p6-iconv): iconv unit, config wire-roundtrip, integration tests 2026-09-10 17:13:06 +02:00
TapTap 6e02a24232 feat(p6-iconv): --iconv charset conversion + PROTOCOL 2.16.0 2026-09-10 17:13:01 +02:00
TapTap 37cff96537 test(p6-stop): unit and integration tests for stop deadlines 2026-09-10 16:27:49 +02:00
TapTap 24d1448246 feat(p6-stop): --stop-after/--stop-at deadline transfer stop 2026-09-10 16:27:49 +02:00
30 changed files with 2158 additions and 110 deletions

No files matched your search

+12 -8
View File
@@ -6,11 +6,11 @@ This document maps rsync's full feature set to FastSync's current implementation
| Status | Count | Description |
|--------|-------|-------------|
| ✅ Implemented | 118 | Feature works end-to-end |
| ✅ Implemented | 121 | Feature works end-to-end |
| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 10 | Flag parsed/stored but behavior incomplete |
| 🔄 Compatibility No-op | 3 | Flag is accepted for CLI compatibility but has no effect |
| ❌ Not Implemented | 13 | Flag not recognized or no behavior |
| ❌ Not Implemented | 10 | Flag not recognized or no behavior |
| **Total** | **147** | |
---
@@ -669,11 +669,11 @@ now transmits targets (the prior behavior was broken/partial); its status moved
| Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------|
| `--stop-after=MINS` | Stop after N minutes | ❌ Not Implemented | |
| `--stop-at=TIME` | Stop at specified time | ❌ Not Implemented | |
| `--stop-after=MINS` | Stop after N minutes | ✅ Implemented | Client-only sender stop deadline (Phase 6): computing `--stop-after=MINS` (a positive minute count; 0/negative/garbage rejected) and `--stop-at=TIME` (`HH:MM`, `HH:MM:SS`, or `now+N[smhd]`; a past time stops immediately). The transfer stops ELEGANTLY at the next chunk boundary: everything already fully sent is kept and applied, the run returns 0, and --delete (late/delete-after timing) does NOT wipe the destination — when the scan is cut short the partial keep-set manifest is suppressed with a warning (the delete walk is skipped rather than acting on an incomplete keep-set, so unscanned source mirrors survive). `--delete-before`/`--delete-during` still run their complete pre-scan (which ignores the deadline). Local client-only fields: never serialized into the wire config frame, so no PROTOCOL_VERSION bump. `--stop-after` uses CLOCK_MONOTONIC; `--stop-at` uses the wall clock. Works single-threaded and under `-m` (multithreaded). Divergence: rsync computes `--stop-after` from the run start; FastSync likewise. When both are given, the earlier of the two deadlines wins (checked per iteration). See the Phase-6 stop notes below |
| `--stop-at=TIME` | Stop at specified time | ✅ Implemented | Same feature as `--stop-after` (deadline transfer stop), absolute wall-clock form (`HH:MM[:SS]` or `now+N[smhd]`). See the row above and the Phase-6 stop notes |
| `--fsync` | Fsync every written file before publication | ✅ Implemented | |
| `--protocol=NUM` | Force older protocol version | ❌ Not Implemented | |
| `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | |
| `--protocol=NUM` | Force older protocol version | ❌ Not Implemented | (Phase 6, deferred) |
| `--iconv=CONVERT_SPEC` | Charset conversion | ✅ Implemented | Charset conversion of FILE NAMES (not content) at the protocol boundary via iconv(3): `--iconv=LOCAL[,REMOTE]` — the sender converts each local filename LOCAL→REMOTE before transmitting, and the receiver converts each wire filename REMOTE→LOCAL before creating/writing. The full CONVERT_SPEC is serialized into the config frame as a new trailing string field so the peer knows the wire charset; **PROTOCOL_VERSION bumped 2.15.0 → 2.16.0**. `LOCAL[,REMOTE]` parse: single charset ⇒ LOCAL==REMOTE (identity both ways); garbage rejected up front. Validation probes BOTH directions (a spec that only opens one way is refused, as is a NUL-emitting target charset like utf-16/utf-32/ucs-2, since filenames cannot contain NUL). An unrepresentable name (EILSEQ/EINVAL) fails that path cleanly with a logged `--iconv: cannot convert file name ...` and is never written mangled/truncated. Conversion is applied at EVERY wire-path site (regular/MKDIR/hardlink path+target/symlink path+target/SPECIAL, the delete manifest, the incremental-check path, and the `-s`/`chunk_serialize` embedded blob path), on both client and server (`--iconv` is also a server/daemon option). Zero overhead when unset. See the Phase-6 iconv notes below |
| `--checksum-seed=NUM` | Set checksum seed | ✅ Implemented | Sets the seed for FastSync's whole-file xxHash64 digest (full 64-bit seed) and for the delta path's per-block xxHash32 strong checksum (low 32 bits of the seed). An explicit seed deterministically changes every computed digest on BOTH endpoints (sender and receiver share the seed via the config frame, protocol 2.10.0), so identical runs with the same seed skip the same files and a changed seed changes the digests — the explicit-seed path that makes xxHash comparisons deterministic. `--checksum-choice=md5` has no seed and ignores it (documented). The value is a strict decimal 0..2⁶⁴-1 (blank, signed, or non-numeric values are rejected). Like rsync, a seed only matters where a digest is actually computed (`--checksum` or a basis-dir run, or a delta transfer); it does not by itself enable `--checksum`/`--delta`. Divergence from rsync: the default is seed 0, and FastSync never randomizes the seed (rsync uses a random per-transfer seed when `--checksum-seed` is unset); FastSync's unset default therefore reproduces its historical byte-for-byte behavior |
| `--secluded-args` | Use protocol to send args | 🔄 Compatibility No-op | Accepted for CLI compatibility; it does not change FastSync transport or protocol behavior. `-s` remains chunk serialization. |
| `--no-OPTION` | Turn off implied option | ✅ Supported | Supported boolean FastSync options and archive-implied options; unsafe or value-taking options are rejected. |
@@ -780,10 +780,14 @@ These are the hardest compatibility items because they require durable formats o
|----------|--------|--------------------|
| `--write-batch=FILE`; `--only-write-batch=FILE`; `--read-batch=FILE` | XL | Specify a versioned batch format, persist all required metadata, and test replay, corruption, and partial application. |
| `--protocol=NUM` | XL | Add protocol-version negotiation and compatibility branches without weakening current validation. |
| `--iconv=CONVERT_SPEC` | L | Convert filenames at the protocol boundary with invalid-sequence and normalization tests. |
| `--stop-after=MINS`; `--stop-at=TIME` | M | Add deadline propagation, interruptible I/O, and safe checkpoint/cleanup behavior. |
| `--iconv=CONVERT_SPEC` | L | ✅ Implemented (see the Advanced table and Phase-6 iconv notes below): filename charset conversion at the wire boundary with expansion/overflow safety and invalid-sequence test coverage |
| `--stop-after=MINS`; `--stop-at=TIME` | M | ✅ Implemented (see the Advanced table and Phase-6 stop notes below): deadline propagation and safe early stop with --delete safety |
| `--early-input=FILE`; `--password-file=FILE` | M | Securely read startup credentials/input with permission checks and no secret disclosure in logs. |
**Phase 6, Wave A (stop deadline) shipping note:** `--stop-after=MINS` and `--stop-at=TIME` are client-only sender stop deadlines. `--stop-after` takes a positive minute count (0/negative/garbage rejected); `--stop-at` takes `HH:MM`, `HH:MM:SS`, or `now+N[smhd]` (a past time stops immediately, a garbage spec is rejected at parse time). The deadline is computed once at the start of the transfer (CLOCK_MONOTONIC for `--stop-after`, wall clock via `time()` for `--stop-at`) and checked at every chunk boundary in both the single-threaded `send_files` loop and the multithreaded `send_chunks_multithreaded` path, and inside the scanner loops so a busy scan itself stops. When it fires, the transfer stops ELEGANTLY: the in-flight chunk completes, the existing completion tail runs (summary, `disconnect`), and the run returns 0 — exactly like rsync's clean early stop. Because the deadline is client-only and never crosses the wire config frame, no PROTOCOL_VERSION bump is required. The safety-critical interaction is with `--delete`: FastSync streams while scanning, so a deadline can cut the source scan short and yield a PARTIAL keep-set manifest; committing that would make the receiver delete destination mirrors of source files not yet scanned. So the sender tracks `scan_stopped_early` and, when it is true on the late/delete-after (`--delete`/`--delete-after`/`--delete-delay`) path, SUPPRESSES the keep-set manifest (logs a warning) so no deletion happens from an incomplete set — this is the safe direction (preserves data; the delete simply does not run). `--delete-before`/`--delete-during` are unaffected: their complete pre-scan runs before any data and ignores the deadline (a stop can be exceeded by that pre-scan). Under `-m` the stop is symmetric and the scanner thread's still-in-progress manifest appends can never race the tail because the tail does not read the manifest on the early-stop path.
**Phase 6, Wave B (iconv) shipping note (PROTOCOL 2.15.0 → 2.16.0):** `--iconv=LOCAL[,REMOTE]` converts file NAMES at the wire boundary (never content). The full CONVERT_SPEC is serialized into the config frame as a new trailing string field (empty→NULL canonicalized), so both ends share the same wire charset interpretation; this required the PROTOCOL bump because the frame is a strict ordered sequence and a peer that does not parse the new trailing field would desynchronize. Each end derives LOCAL (its own charset) and REMOTE (the wire charset): the sender opens LOCAL→REMOTE and converts every transmitted filename; the receiver opens REMOTE→LOCAL and converts every received filename before creating/writing. Conversion is applied at every wire-path site (regular/MKDIR/hardlink path+target/symlink path+target/SPECIAL, the delete manifest keep/protected/missing entries, the incremental-check path, and the embedded `-s`/chunk-blob path). A name it cannot convert (EILSEQ/EINVAL) is failed cleanly with a logged `--iconv: cannot convert file name ...` and is never written truncated/mangled. Validation probes both directions up front (both the sender local→remote and the receiver remote→local, and, for a server/daemon with its own `--iconv`, the client-REMOTE→server-LOCAL pair) so an unusable spec is rejected before the connection rather than mid-transfer, and NUL-emitting target charsets (utf-16/utf-32/ucs-2) are refused because filenames cannot contain NUL. Divergence documented upstream: the receiver does NOT half-swap; the wire charset always comes from the sender's REMOTE half, so a server whose local charset differs from the client's LOCAL must declare it with its own `--iconv`. Conversion is process-global and runs on a single thread per process (sender thread / receiver-loop thread), initialized before worker threads start and freed after they join.
### Recommended Delivery Order
1. Resolve short-option conflicts (`-m`, `-M`, `-T`, `-f`, `-s`) and define the compatibility contract.
+61
View File
@@ -1,5 +1,6 @@
#include "client_send.h"
#include "client_validation.h"
#include "charset.h"
#include "chmod.h"
#include "compression.h"
#include "config.h"
@@ -11,12 +12,14 @@
#include "identity.h"
#include "log.h"
#include "protocol.h"
#include "stop_condition.h"
#include "transport_tcp.h"
#include "transport_tls.h"
#include "usage.h"
#include "utils.h"
#include <errno.h>
#include <limits.h>
#include <time.h>
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
@@ -590,6 +593,11 @@ static const OptionEntry OPTION_TABLE[] = {
* path; main() reads it (after the destination form is known) and derives
* the wire credentials. Never crosses the wire. */
{"--password-file", NULL, OPT_STRING, offsetof(Config, password_file)},
/* --iconv (protocol 2.16.0): convert file-NAME charsets at the wire
* boundary. The CONVERT_SPEC (LOCAL[,REMOTE]) is validated for real iconv
* charsets at startup (client_validation.c) and the full spec rides the
* config frame so the receiver derives the wire charset symmetrically. */
{"--iconv", NULL, OPT_STRING, offsetof(Config, iconv_spec)},
{"--delete-before", NULL, OPT_FLAG, offsetof(Config, delete_before)},
{"--delete-during", "--del", OPT_FLAG, offsetof(Config, delete_during)},
{"--delete-delay", NULL, OPT_FLAG, offsetof(Config, delete_delay)},
@@ -847,6 +855,47 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
return -1;
continue;
}
/* --stop-after/--stop-at are client-only sender-side stop deadlines. They
* are parsed by stop_condition (so the unit tests exercise the same validate
* that production uses) and never serialized into the config frame. */
if (strncmp(argv[i], "--stop-after=", 13) == 0) {
if (!stop_parse_after_minutes(argv[i] + 13, &config->stop_after_mins)) {
log_message(LOG_LEVEL_ERROR, "--stop-after must be a positive number of minutes");
return -1;
}
continue;
}
if (strcmp(argv[i], "--stop-after") == 0) {
if (i + 1 >= argc) {
log_message(LOG_LEVEL_ERROR, "missing argument for --stop-after");
return -1;
}
if (!stop_parse_after_minutes(argv[++i], &config->stop_after_mins)) {
log_message(LOG_LEVEL_ERROR, "--stop-after must be a positive number of minutes");
return -1;
}
continue;
}
if (strncmp(argv[i], "--stop-at=", 10) == 0) {
if (!stop_parse_at_time(argv[i] + 10, time(NULL), &config->stop_at)) {
log_message(LOG_LEVEL_ERROR, "--stop-at must be HH:MM[:SS] or now+N[smhd]");
return -1;
}
config->stop_at_set = true;
continue;
}
if (strcmp(argv[i], "--stop-at") == 0) {
if (i + 1 >= argc) {
log_message(LOG_LEVEL_ERROR, "missing argument for --stop-at");
return -1;
}
if (!stop_parse_at_time(argv[++i], time(NULL), &config->stop_at)) {
log_message(LOG_LEVEL_ERROR, "--stop-at must be HH:MM[:SS] or now+N[smhd]");
return -1;
}
config->stop_at_set = true;
continue;
}
const char* threads_prefix = "--compress-threads=";
if (strncmp(argv[i], threads_prefix, strlen(threads_prefix)) == 0) {
if (set_compression_threads_option(&config->compression_threads,
@@ -1593,6 +1642,17 @@ int main(int argc, char* argv[]) {
goto cleanup;
}
/* --iconv: install the sender-side local->wire conversion before any path is
scanned or serialized (the scanner and the chunk/data path read windows are
all driven from this process, so one global initialization covers every
send site). */
if (!charset_wire_init_sender(config->iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv has an invalid CONVERT_SPEC or an unsupported charset name");
exit_code = 1;
goto cleanup;
}
/* Apply the requested --outbuf style now that the mode is parsed. */
apply_output_buffering(config);
@@ -1614,6 +1674,7 @@ int main(int argc, char* argv[]) {
}
cleanup:
charset_wire_free();
if (config) {
config_delete(config);
}
+157 -79
View File
@@ -1,6 +1,7 @@
#include "client_send.h"
#include "array_list.h"
#include "change_list.h"
#include "charset.h"
#include "chunk.h"
#include "compression.h"
#include "config.h"
@@ -17,6 +18,7 @@
#include "protocol.h"
#include "queue.h"
#include "scanner.h"
#include "stop_condition.h"
#include "transport_tcp.h"
#include "transport_ssh.h"
#include "transport_tls.h"
@@ -810,21 +812,21 @@ static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protecte
if (!send_int(fd, keep_count))
return -1;
for (int i = 0; i < keep_count; i++) {
if (!send_str(fd, (char*)manifest->items[i]))
if (!send_wire_str(fd, (char*)manifest->items[i]))
return -1;
}
int protected_count = protected_prefixes ? protected_prefixes->size : 0;
if (!send_int(fd, protected_count))
return -1;
for (int i = 0; i < protected_count; i++) {
if (!send_str(fd, (char*)protected_prefixes->items[i]))
if (!send_wire_str(fd, (char*)protected_prefixes->items[i]))
return -1;
}
int missing_count = missing_args ? missing_args->size : 0;
if (!send_int(fd, missing_count))
return -1;
for (int i = 0; i < missing_count; i++) {
if (!send_str(fd, (char*)missing_args->items[i]))
if (!send_wire_str(fd, (char*)missing_args->items[i]))
return -1;
}
return 0;
@@ -899,7 +901,7 @@ static int incremental_check(Client* client, File* file, const Config* config,
*resume_offset = 0;
if (!send_status(client->file_descriptor, STATUS_CHECK))
return -1;
if (!send_str(client->file_descriptor, file_wire_path(file)))
if (!send_wire_str(client->file_descriptor, file_wire_path(file)))
return -1;
unsigned long long fsize = file->data->size;
long long mtime = file->metadata ? file->metadata->mtime_sec : 0;
@@ -1119,7 +1121,7 @@ static bool send_directory_entry(Client* client, File* file) {
return false;
if (!send_status(client->file_descriptor, STATUS_MKDIR))
return false;
return send_str(client->file_descriptor, file_wire_path(file));
return send_wire_str(client->file_descriptor, file_wire_path(file));
}
/* Transmit one symlink entry: a STATUS_SYMLINK frame carrying the destination
@@ -1130,8 +1132,8 @@ static bool send_symlink_entry(const Client* client, File* file, const Config* c
if (!file || !file_wire_path(file) || !file->symlink_target)
return false;
int fd = client->file_descriptor;
if (!send_status(fd, STATUS_SYMLINK) || !send_str(fd, file_wire_path(file)) ||
!send_str(fd, file->symlink_target))
if (!send_status(fd, STATUS_SYMLINK) || !send_wire_str(fd, file_wire_path(file)) ||
!send_wire_str(fd, file->symlink_target))
return false;
return !config->use_metadata || metadata_send(fd, file->metadata);
}
@@ -1311,9 +1313,9 @@ static int send_chunk_with_removal(Client* client, Chunk* chunk, Config* config,
wire path so the receiver links this entry to that installed file. */
if (f->link_group != 0 && !f->link_first && f->hardlink_target != NULL) {
if (!send_status(client->file_descriptor, STATUS_HARDLINK) ||
!send_str(client->file_descriptor, file_wire_path(f)) ||
!send_wire_str(client->file_descriptor, file_wire_path(f)) ||
!send_int(client->file_descriptor, f->link_group) ||
!send_str(client->file_descriptor, f->hardlink_target))
!send_wire_str(client->file_descriptor, f->hardlink_target))
return -1;
change_emit_file_sent(config, f);
continue;
@@ -1396,6 +1398,16 @@ static int send_chunks_multithreaded(void* pipeline_context) {
}
while (true) {
/* Phase 6: stop-elegantly at the next chunk boundary once the --stop-after
/ --stop-at deadline has passed. Everything already sent is finalized by
the completion tail below; the run still returns success. */
if (stop_condition_reached(&context->stop_condition)) {
log_info_message(LOG_INFO_MISC,
"Stop deadline reached; stopping transfer at the next chunk boundary");
context->scan_stopped_early = true;
pipeline_cancel(context);
break;
}
Chunk* current_chunk = queue_dequeue_multithreaded(
context->queue_loader, &context->mutex_loader, &context->condition_not_empty_loader,
&context->condition_not_full_loader, &context->loader_done);
@@ -1407,55 +1419,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
protocol_session_unbind();
return thrd_error;
}
if (context->config->use_delete && !context->early_delete) {
/* Empty keep-set + scan I/O error must not delete the whole destination
(the source may not be genuinely empty -- see send_files). */
bool empty_io;
mtx_lock(&context->mutex_scanner);
empty_io = context->scan_had_io_error && context->manifest && context->manifest->size == 0;
mtx_unlock(&context->mutex_scanner);
if (empty_io) {
log_message(LOG_LEVEL_ERROR,
"source scan hit an I/O error before finding any file; refusing to delete "
"with an empty keep-set (--delete)");
goto send_fail;
}
if (send_delete_manifest(client->file_descriptor, context->manifest,
context->excluded_paths, context->missing_args) != 0)
goto send_fail;
} else if (context->config->delete_missing_args && !context->early_delete) {
/* --delete-missing-args without --delete: no keep-set is built, but the
exact-delete paths still ride the same manifest frame (commit once the
transfer succeeded). */
if (send_delete_manifest(client->file_descriptor, NULL, NULL, context->missing_args) != 0)
goto send_fail;
}
bool ok = finalize_transfer(client, context->config, context->remove_source_files);
if (!ok && context->config->use_delete)
log_message(LOG_LEVEL_ERROR,
"server reported a deletion failure (--delete); see the server log for the "
"reason (a --max-delete limit that the run would exceed deletes nothing)");
if (ok)
remove_transferred_sources(context->config, context->remove_source_files);
mtx_lock(&context->mutex_progress);
int total_files = context->total_files;
unsigned long long total_bytes = context->total_bytes;
mtx_unlock(&context->mutex_progress);
if (context->config->stats)
fprintf(stderr, "Stats: %d files, %.1f MB\n", total_files, total_bytes / 1048576.0);
log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files,
total_bytes / 1048576.0);
disconnect_transfer_client(client);
mark_sender_done(context);
protocol_session_unbind();
return ok ? thrd_success : thrd_error;
send_fail:
pipeline_cancel(context);
disconnect_transfer_client(client);
mark_sender_done(context);
protocol_session_unbind();
return thrd_error;
break;
}
if (send_chunk_with_removal(client, current_chunk, context->config,
context->remove_source_files) != 0) {
@@ -1482,6 +1446,72 @@ static int send_chunks_multithreaded(void* pipeline_context) {
mtx_unlock(&context->mutex_progress);
chunk_destroy(current_chunk);
}
/* Completion tail: reached on natural exhaustion or an early stop deadline.
A deadline that cut the scan short leaves an incomplete keep-set manifest;
transmitting it would make the receiver --delete the unscanned source
mirrors (data loss), so it is deliberately suppressed. Suppressing it also
means the manifest (which the scanner thread may still be appending) is
never read here on the early-stop path, so no scanner synchronization is
required to enter the tail. */
context->scan_stopped_early =
context->scan_stopped_early || stop_condition_reached(&context->stop_condition);
if (context->scan_stopped_early) {
if (context->config->use_delete || context->config->delete_missing_args)
log_message(LOG_LEVEL_WARNING,
"transfer stopped early (stop deadline); skipping --delete keep-set so "
"unscanned source mirrors are not deleted");
else
log_message(LOG_LEVEL_WARNING, "transfer stopped early (stop deadline)");
} else if (context->config->use_delete && !context->early_delete) {
/* Empty keep-set + scan I/O error must not delete the whole destination
(the source may not be genuinely empty -- see send_files). */
bool empty_io;
mtx_lock(&context->mutex_scanner);
empty_io = context->scan_had_io_error && context->manifest && context->manifest->size == 0;
mtx_unlock(&context->mutex_scanner);
if (empty_io) {
log_message(LOG_LEVEL_ERROR,
"source scan hit an I/O error before finding any file; refusing to delete "
"with an empty keep-set (--delete)");
goto send_fail;
}
if (send_delete_manifest(client->file_descriptor, context->manifest, context->excluded_paths,
context->missing_args) != 0)
goto send_fail;
} else if (context->config->delete_missing_args && !context->early_delete) {
/* --delete-missing-args without --delete: no keep-set is built, but the
exact-delete paths still ride the same manifest frame (commit once the
transfer succeeded). */
if (send_delete_manifest(client->file_descriptor, NULL, NULL, context->missing_args) != 0)
goto send_fail;
}
bool ok = finalize_transfer(client, context->config, context->remove_source_files);
if (!ok && context->config->use_delete)
log_message(LOG_LEVEL_ERROR,
"server reported a deletion failure (--delete); see the server log for the "
"reason (a --max-delete limit that the run would exceed deletes nothing)");
if (ok)
remove_transferred_sources(context->config, context->remove_source_files);
mtx_lock(&context->mutex_progress);
int total_files = context->total_files;
unsigned long long total_bytes = context->total_bytes;
mtx_unlock(&context->mutex_progress);
if (context->config->stats)
fprintf(stderr, "Stats: %d files, %.1f MB\n", total_files, total_bytes / 1048576.0);
log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files,
total_bytes / 1048576.0);
disconnect_transfer_client(client);
mark_sender_done(context);
protocol_session_unbind();
return ok ? thrd_success : thrd_error;
send_fail:
pipeline_cancel(context);
disconnect_transfer_client(client);
mark_sender_done(context);
protocol_session_unbind();
return thrd_error;
}
/* Scan thread of the -m pipeline. --dirs disables recursive traversal (the
@@ -1496,6 +1526,7 @@ static int scan_directory_multithreaded(void* pipeline_context) {
protocol_session_unbind();
return thrd_error;
}
prepared.options.stop_condition = &context->stop_condition;
/* The keep-set manifest for the late modes is built from this data pass, so
the parallel scanner records the protected excluded prefixes here. The
early modes already transmitted the pre-scan keep-set and its protected
@@ -1790,6 +1821,17 @@ int send_files(Config* config) {
if (!manifest)
goto send_fail;
}
/* Phase 6: compute the client-only stop deadline once at transfer start. The
early-delete pre-scan above deliberately ignores it so the keep-set (and
its committed deletion) is always complete and correct. */
struct timespec now_mono;
if (clock_gettime(CLOCK_MONOTONIC, &now_mono) != 0) {
now_mono.tv_sec = 0;
now_mono.tv_nsec = 0;
}
StopCondition stop = stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins,
config->stop_at_set, config->stop_at, now_mono);
prepared.options.stop_condition = &stop;
scanner = directory_scanner_create_with_options(config->send_directory, &prepared.options);
if (!scanner)
goto send_fail;
@@ -1799,7 +1841,20 @@ int send_files(Config* config) {
int total_files = 0;
time_t last_progress = 0;
time_t start = time(NULL);
/* True when the stop deadline cut the scan short so the keep-set manifest is
only a prefix of the source. */
bool scan_stopped_early = false;
while ((current_chunk = directory_scanner_next(scanner)) != NULL) {
/* Phase 6: stop-elegantly at the next chunk boundary once the deadline has
passed. The scanner may also have stopped early itself; either way the
completion tail below keeps everything already sent. */
if (stop_condition_reached(&stop)) {
chunk_destroy(current_chunk);
log_info_message(LOG_INFO_MISC,
"Stop deadline reached; stopping transfer at the next chunk boundary");
scan_stopped_early = true;
break;
}
unsigned long long chunk_bytes = 0;
for (int i = 0; i < current_chunk->element_count; i++) {
chunk_bytes += current_chunk->items[i]->data->size;
@@ -1853,31 +1908,46 @@ int send_files(Config* config) {
goto send_fail;
if (directory_scanner_had_io_error(scanner))
had_scan_io = true;
if (had_scan_io && manifest && manifest->size == 0) {
/* A scan that hit an I/O error and produced no keep entries is ambiguous;
an empty keep-set would delete the whole destination. Refuse to delete
(see the early-timing comment above). */
log_message(LOG_LEVEL_ERROR,
"source scan hit an I/O error before finding any file; refusing to delete with "
"an empty keep-set (--delete)");
goto send_fail;
}
if ((manifest || config->delete_missing_args) && !delete_early) {
/* Late (commit) ordering: all file data is out; transmit the manifest so
the receiver commits the extras walk (--delete) and/or the
--delete-missing-args exact-path deletions only after the transfer
succeeds. In the early modes (--delete-before/--delete-during) the
manifest already went out up front, so nothing is re-sent here. */
if (send_delete_manifest(client->file_descriptor, manifest, excluded, missing_args) != 0) {
/* Phase 6: the scanner may have stopped early (returning NULL without a
failure) as soon as the deadline passed, so reflect that here too. A
deadline that cut the scan short leaves an incomplete keep-set; transmitting
it would make the receiver --delete the unscanned source mirrors (data
loss), so the late delete manifest is suppressed below. */
scan_stopped_early = scan_stopped_early || stop_condition_reached(&stop);
if (scan_stopped_early) {
if (config->use_delete || config->delete_missing_args)
log_message(LOG_LEVEL_WARNING,
"transfer stopped early (stop deadline); skipping --delete keep-set so "
"unscanned source mirrors are not deleted");
else
log_message(LOG_LEVEL_WARNING, "transfer stopped early (stop deadline)");
} else {
if (had_scan_io && manifest && manifest->size == 0) {
/* A scan that hit an I/O error and produced no keep entries is ambiguous;
an empty keep-set would delete the whole destination. Refuse to delete
(see the early-timing comment above). */
log_message(LOG_LEVEL_ERROR,
"source scan hit an I/O error before finding any file; refusing to delete with "
"an empty keep-set (--delete)");
goto send_fail;
}
if ((manifest || config->delete_missing_args) && !delete_early) {
/* Late (commit) ordering: all file data is out; transmit the manifest so
the receiver commits the extras walk (--delete) and/or the
--delete-missing-args exact-path deletions only after the transfer
succeeds. In the early modes (--delete-before/--delete-during) the
manifest already went out up front, so nothing is re-sent here. */
if (send_delete_manifest(client->file_descriptor, manifest, excluded, missing_args) != 0) {
if (manifest) {
array_list_delete(manifest);
manifest = NULL;
}
goto send_fail;
}
if (manifest) {
array_list_delete(manifest);
manifest = NULL;
}
goto send_fail;
}
if (manifest) {
array_list_delete(manifest);
manifest = NULL;
}
}
bool ok = finalize_transfer(client, config, remove_sources);
@@ -1987,6 +2057,14 @@ int send_files_multithreaded(Config** config_ptr) {
context->missing_args = missing_args;
missing_args = NULL; /* owned by the context from here on */
*config_ptr = NULL; /* context now owns config through all remaining paths */
struct timespec now_mono;
if (clock_gettime(CLOCK_MONOTONIC, &now_mono) != 0) {
now_mono.tv_sec = 0;
now_mono.tv_nsec = 0;
}
context->stop_condition =
stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins, config->stop_at_set,
config->stop_at, now_mono);
bool collect_excluded = config->use_delete && !config->delete_excluded;
if (config->use_delete) {
context->manifest = array_list_create(free);
+9
View File
@@ -1,4 +1,5 @@
#include "client_validation.h"
#include "charset.h"
#include "delay_updates.h"
#include "log.h"
#include "usage.h"
@@ -123,5 +124,13 @@ bool validate_config(const Config* config) {
"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");
return false;
}
return true;
}
+11
View File
@@ -487,6 +487,7 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
scanner->relative_mode = options->relative && options->file_list != NULL;
scanner->hardlinks = options->hardlinks;
scanner->prune_empty_dirs = options->prune_empty_dirs;
scanner->stop_condition = options->stop_condition;
scanner->dirs_root_emitted = false;
scanner->list_index = 0;
scanner->dirs_batch = NULL;
@@ -843,6 +844,12 @@ static Chunk* dirs_flush_batch(DirectoryScanner* scanner) {
static Chunk* directory_scanner_next_dirs(DirectoryScanner* scanner) {
while (scanner->dirs_batch == NULL || scanner->dirs_batch_size <= scanner->chunk_size) {
if (scanner->stop_condition && stop_condition_reached(scanner->stop_condition)) {
Chunk* leftover = dirs_flush_batch(scanner);
if (leftover)
chunk_destroy(leftover);
return NULL;
}
if (!scanner->dirs_batch) {
scanner->dirs_batch = array_list_create(file_destroy);
if (!scanner->dirs_batch) {
@@ -886,6 +893,10 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
unsigned long long chunk_data_size = 0;
while (1) {
if (scanner->stop_condition && stop_condition_reached(scanner->stop_condition)) {
array_list_delete(chunk_data);
return NULL;
}
if (scanner->current_dir == NULL) {
int ret = open_next_directory(scanner);
if (ret == 0)
+8
View File
@@ -7,6 +7,7 @@
#include "hardlink.h"
#include "protocol.h"
#include "queue.h"
#include "stop_condition.h"
#include <dirent.h>
#include <stdbool.h>
#include <stdatomic.h>
@@ -92,6 +93,11 @@ typedef struct {
* read-only here; the parallel scanner passes it unchanged to every worker so
* one table detects every group across all subdirectories. */
HardLinkTable* hardlinks;
/* Phase 6: optional sender stop deadline. When non-NULL the scanner checks
* it at natural loop boundaries and stops emitting chunks once reached
* (without marking the scan as failed), so a busy scan itself stops early.
* Client-only, never serialized to the wire. */
const StopCondition* stop_condition;
} ScannerOptions;
/* Internal per-scanner filter state. FilterNode chains represent the ordered
@@ -165,6 +171,8 @@ typedef struct {
/* --hard-links (-H): shared link-group detection table (see ScannerOptions).
NULL when -H is off. */
HardLinkTable* hardlinks;
/* Phase 6: sender stop deadline (from ScannerOptions). */
const StopCondition* stop_condition;
} DirectoryScanner;
typedef struct {
+13
View File
@@ -35,6 +35,12 @@ void print_usage(void) {
printf(" --progress Show transfer progress\n");
printf(" -P Partial mode with progress (retention incomplete)\n");
printf(" -8, --8-bit-output Leave high-bit characters unescaped in output\n");
printf(" --iconv=LOCAL[,REMOTE] Convert file-NAME charsets at the wire boundary:\n");
printf(" LOCAL is the charset of our file names, REMOTE is the\n");
printf(" remote side's charset (defaults to LOCAL). Names are\n");
printf(" converted before transmission and back on receipt; a\n");
printf(" name that cannot be represented in the target charset\n");
printf(" fails that transfer cleanly (rsync-compatible)\n");
printf(" --delete Delete files on receiver not in source\n");
printf(" (default timing: delete only after the whole\n");
printf(" transfer has succeeded)\n");
@@ -184,6 +190,13 @@ void print_usage(void) {
printf(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
printf(" -T <sec> Alias for --timeout\n");
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\n");
printf(" --stop-after=MINS Stop the transfer after MINS minutes (a positive\n");
printf(" integer); whatever was already transferred is kept\n");
printf(" --stop-at=TIME Stop at an absolute time: HH:MM, HH:MM:SS, or\n");
printf(" now+N[smhd] (a time already in the past stops the\n");
printf(" transfer immediately; client-only). An early stop\n");
printf(" skips the late --delete keep-set so it cannot delete\n");
printf(" source mirrors that were not yet scanned\n");
printf(" --address <ip> Bind the outgoing client socket to this source address\n");
printf(" -4, --ipv4 Force IPv4 for destination resolution\n");
printf(" -6, --ipv6 Force IPv6 for destination resolution\n");
+2 -1
View File
@@ -1,5 +1,6 @@
#include "receiver.h"
#include "charset.h"
#include "chunk.h"
#include "config.h"
#include "delay_updates.h"
@@ -79,7 +80,7 @@ static bool receiver_process_batch(Config* config, int file_descriptor) {
count > MAX_MANIFEST_ENTRIES)
return false;
for (int i = 0; i < count; i++) {
char* check_path = receive_str(file_descriptor);
char* check_path = receive_wire_str(file_descriptor);
if (!check_path)
return false;
unsigned long long check_size;
+35
View File
@@ -1,4 +1,5 @@
#include "config.h"
#include "charset.h"
#include "credentials.h"
#include "daemon_conf.h"
#include "delay_updates.h"
@@ -31,6 +32,10 @@ static bool allow_delete;
static bool trust_sender;
static bool allow_unauthenticated;
static const char* required_client_cn;
/* --iconv CONVERT_SPEC the server was itself started with (borrowed argv
* pointer). Its LOCAL half may override the local charset the client assumed;
* see charset_wire_init_receiver. */
static const char* server_iconv_spec;
/* Non-NULL exactly when the listener runs in --daemon mode. Loaded once in
* main before any accept-loop fork, then shared read-only by every forked
@@ -170,6 +175,15 @@ static const char* server_module_gate(const Config* config, void* context) {
ModuleGateContext* gate_ctx = (ModuleGateContext*)context;
if (!config)
return "missing config frame";
/* --iconv (protocol 2.16.0): the receiver's exact conversion direction (the
client spec's wire charset into this server's local charset, including a
server-side --iconv override) must be usable BEFORE the STATUS_OK ack, so
an impossible conversion is refused at the handshake instead of failing
the first file mid-transfer. The client spec itself was already sanity
checked by validate_received_config. */
if (config->iconv_spec &&
!charset_wire_receiver_spec_valid(config->iconv_spec, server_iconv_spec))
return "client --iconv conversion cannot be honored by this server";
bool is_daemon = g_daemon_conf != NULL;
bool has_module = config->module != NULL && config->module[0] != '\0';
@@ -318,6 +332,20 @@ void handler(int file_descriptor) {
return;
}
config->use_delete = config->use_delete && allow_delete;
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
now that the client's full CONVERT_SPEC has been received and validated,
before any received file name is decoded. The server's own --iconv (if
any) may override the local charset; a spec the client is known to have
validated cannot fail here unless the server's override names an
unsupported charset. */
if (config->iconv_spec && !charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
log_message(LOG_LEVEL_ERROR,
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
config_delete(config);
close(file_descriptor);
protocol_session_unbind();
return;
}
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
deletion and stays gated by the same --allow-delete server policy. When
the server policy is off the flag is inert (the missing entries are still
@@ -485,6 +513,7 @@ void handler(int file_descriptor) {
}
protocol_session_unbind();
identity_clear_active();
charset_wire_free();
close(file_descriptor);
}
@@ -535,6 +564,11 @@ static void print_server_usage(void) {
printf(" -6, --ipv6 Bind an IPv6 socket\n");
printf(" --allow-delete Permit manifest deletion\n");
printf(" --trust-sender Trust the remote sender's file list\n");
printf(" --iconv=LOCAL[,REMOTE] Declare this server's LOCAL charset for file-name\n");
printf(" conversion: received names are translated to this\n");
printf(" charset (the wire charset still comes from the\n");
printf(" client's CONVERT_SPEC). A name that cannot be\n");
printf(" represented fails the run cleanly\n");
printf(" --allow-unauthenticated Allow plaintext/anonymous network clients\n");
printf(" -v, --verbose Enable debug logging\n");
printf(" --help Show this help\n");
@@ -621,6 +655,7 @@ int main(int argc, char* argv[]) {
allow_delete = opts.allow_delete;
trust_sender = opts.trust_sender;
allow_unauthenticated = opts.allow_unauthenticated;
server_iconv_spec = opts.iconv_spec;
signal(SIGINT, cleanup);
signal(SIGTERM, cleanup);
+22
View File
@@ -1,4 +1,5 @@
#include "server_cli.h"
#include "charset.h"
#include "utils.h"
#include <limits.h>
#include <stdarg.h>
@@ -143,6 +144,12 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
opts->trust_sender = true;
} else if (arg_is(argv[i], "--allow-unauthenticated")) {
opts->allow_unauthenticated = true;
} else if (arg_is(argv[i], "--iconv")) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for --iconv");
return -1;
}
opts->iconv_spec = argv[++i];
} else if (arg_is(argv[i], "-p")) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for -p");
@@ -180,6 +187,15 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
inline_value = argv[++i];
}
opts->early_input_file = inline_value;
} else if (arg_has_value(argv[i], "--iconv", &inline_value)) {
if (!inline_value) {
if (i + 1 >= argc) {
set_error(err, err_size, "missing argument for --iconv");
return -1;
}
inline_value = argv[++i];
}
opts->iconv_spec = inline_value;
} else if (arg_has_value(argv[i], "--dparam", &inline_value)) {
if (!inline_value) {
if (i + 1 >= argc) {
@@ -227,6 +243,12 @@ int server_cli_parse(int argc, char* argv[], ServerCliOptions* opts, char* err,
"--daemon");
return -1;
}
/* --iconv: reject a malformed CONVERT_SPEC or an unsupported charset name at
startup (a probe iconv_open is attempted). */
if (opts->iconv_spec != NULL && !charset_spec_valid(opts->iconv_spec)) {
set_error(err, err_size, "--iconv requires LOCAL[,REMOTE] charset names supported by iconv");
return -1;
}
return 0;
}
+6
View File
@@ -33,6 +33,12 @@ typedef struct ServerCliOptions {
bool allow_delete; /* --allow-delete */
bool trust_sender; /* --trust-sender */
bool allow_unauthenticated; /* --allow-unauthenticated */
/* --iconv=CONVERT_SPEC: the server's own LOCAL charset declaration. The
* client's full spec rides the wire config frame anyway; when the server is
* started with its own --iconv, its LOCAL half overrides the local charset
* the client assumed so the server converts received names to ITS charset.
* Borrowed pointer into argv (never owns heap). */
const char* iconv_spec; /* --iconv value, or NULL */
} ServerCliOptions;
/* Parse argc/argv into *opts. Zero-initialize *opts before calling (or use
+384
View File
@@ -0,0 +1,384 @@
#include "charset.h"
#include "log.h"
#include "protocol.h"
#include "utils.h"
#include <errno.h>
#include <iconv.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
iconv_t cd;
} CharsetConversion;
/* Process-wide wire conversion descriptor (one direction per process: a client
* only sends, a server only receives). CONCURRENCY CONTRACT: iconv_t is not
* guaranteed thread-safe, so every conversion MUST run on a single thread at a
* time. This holds today -- on the client the conversions run on the sender
* thread (in the -m pipeline chunk_serialize/send happen on the sender thread
* only), on the server on the receive-loop thread; the descriptor is
* initialized on one thread before any transfer thread spawns and torn down
* (charset_wire_free) only after all threads have joined. Do not add a
* concurrent conversion path (e.g. parallel chunk serialization) without
* guarding access with a mutex. */
static CharsetConversion* g_wire_conv;
/* Grow *buf to double capacity, freeing it on failure. realloc preserves the
* already-written prefix, so the caller only tracks its write offset. */
static bool grow_charset_buffer(char** buf, size_t* cap) {
size_t new_cap = *cap * 2;
if (new_cap <= *cap) {
free(*buf);
*buf = NULL;
return false;
}
char* grown = realloc(*buf, new_cap);
if (!grown) {
free(*buf);
*buf = NULL;
return false;
}
*buf = grown;
*cap = new_cap;
return true;
}
/* Throw away any pending shift state so a subsequent conversion starts clean.
* The flush output is discarded; for the stateless single-byte/UTF charsets
* this feature targets it is a no-op. */
static void charset_conversion_reset(const CharsetConversion* conv) {
char scratch[64];
char* sp = scratch;
size_t sl = sizeof(scratch);
(void)iconv(conv->cd, NULL, NULL, &sp, &sl);
}
int charset_spec_parse(const char* spec, char** local_out, char** remote_out) {
if (!local_out || !remote_out)
return -1;
*local_out = NULL;
*remote_out = NULL;
if (!spec || spec[0] == '\0')
return -1;
char* dup = str_dup(spec);
if (!dup)
return -1;
char* comma = strchr(dup, ',');
if (comma) {
if (comma == dup || comma[1] == '\0') {
free(dup);
return -1;
}
*comma = '\0';
*local_out = str_dup(dup);
*remote_out = str_dup(comma + 1);
free(dup);
} else {
*local_out = str_dup(dup);
*remote_out = str_dup(dup);
free(dup);
}
if (!*local_out || !*remote_out) {
free(*local_out);
free(*remote_out);
*local_out = NULL;
*remote_out = NULL;
return -1;
}
return 0;
}
void* charset_conversion_open(const char* from_charset, const char* to_charset) {
if (!from_charset || !to_charset)
return NULL;
iconv_t cd = iconv_open(to_charset, from_charset);
if (cd == (iconv_t)-1)
return NULL;
CharsetConversion* conv = malloc(sizeof(CharsetConversion));
if (!conv) {
iconv_close(cd);
return NULL;
}
conv->cd = cd;
return conv;
}
void charset_conversion_close(void* conversion) {
if (!conversion)
return;
CharsetConversion* conv = (CharsetConversion*)conversion;
iconv_close(conv->cd);
free(conv);
}
/* Probe a single conversion direction: the from/to charsets both open AND a
* representative ASCII name converts to a byte string containing no embedded
* NUL (so a target charset like UTF-16 that emits NUL bytes for ordinary ASCII
* names is rejected up front -- such an output would be silently truncated by
* the C-string wire helpers). */
static bool direction_probe_valid(const char* from, const char* to) {
if (!from || !to)
return false;
void* conv = charset_conversion_open(from, to);
if (!conv)
return false;
bool ok = true;
char input = 'a';
char* in_ptr = &input;
size_t in_left = 1;
char out_buf[64];
char* out_ptr = out_buf;
size_t out_left = sizeof(out_buf);
if (iconv(((CharsetConversion*)conv)->cd, &in_ptr, &in_left, &out_ptr, &out_left) == (size_t)-1)
ok = false;
char flush_buf[64];
char* flush_ptr = flush_buf;
size_t flush_left = sizeof(flush_buf);
if (ok &&
iconv(((CharsetConversion*)conv)->cd, NULL, NULL, &flush_ptr, &flush_left) == (size_t)-1)
ok = false;
size_t produced = (size_t)(out_ptr - out_buf);
if (ok && produced > 0 && memchr(out_buf, '\0', produced) != NULL)
ok = false;
charset_conversion_close(conv);
return ok;
}
bool charset_pair_valid(const char* local, const char* remote) {
/* Both ends convert in opposite directions with the same two charsets, so a
* valid spec must open (and be NUL-free) in BOTH directions: the sender
* opens local->remote, the receiver opens remote->local. */
return direction_probe_valid(local, remote) && direction_probe_valid(remote, local);
}
bool charset_spec_valid(const char* spec) {
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
bool ok = charset_pair_valid(local, remote);
free(local);
free(remote);
return ok;
}
bool charset_spec_valid_direction(const char* from_charset, const char* to_charset) {
return direction_probe_valid(from_charset, to_charset);
}
/* The receiver's real conversion is wire(client REMOTE) -> server-local (the
* server's own --iconv LOCAL half, or the client's LOCAL half when the server
* has no --iconv). A dedicated pre-ack check so an impossible direction is
* rejected before the connection instead of refusing mid-transfer. */
bool charset_wire_receiver_spec_valid(const char* spec, const char* server_spec) {
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
const char* wire = remote;
const char* target_local = local;
char* server_local = NULL;
char* server_remote = NULL;
if (server_spec) {
if (charset_spec_parse(server_spec, &server_local, &server_remote) != 0) {
free(local);
free(remote);
return false;
}
target_local = server_local;
}
bool ok = charset_spec_valid_direction(wire, target_local);
free(server_local);
free(server_remote);
free(local);
free(remote);
return ok;
}
char* charset_convert(const void* conversion, const char* in, int* err_out) {
if (!conversion || !in)
return NULL;
const CharsetConversion* conv = (const CharsetConversion*)conversion;
size_t in_len = strlen(in);
size_t cap = in_len + 16;
char* out = malloc(cap);
if (!out)
return NULL;
size_t in_left = in_len;
char* in_ptr = (char*)in;
size_t out_used = 0;
while (in_left > 0) {
char* out_ptr = out + out_used;
size_t out_left = cap - out_used;
if (iconv(conv->cd, &in_ptr, &in_left, &out_ptr, &out_left) == (size_t)-1) {
if (errno != E2BIG) {
if (err_out)
*err_out = errno;
charset_conversion_reset(conv);
free(out);
return NULL;
}
/* Output exhausted but input remains. E2BIG does not roll the output
pointer back: the bytes iconv already emitted before the failure must
be preserved, so advance out_used before growing. */
out_used = (size_t)(out_ptr - out);
if (!grow_charset_buffer(&out, &cap))
return NULL;
continue;
}
out_used = (size_t)(out_ptr - out);
}
/* Flush any pending shift state (a no-op for the stateless single-byte and
UTF charsets this feature targets, but keeps the descriptor clean). */
for (;;) {
char* out_ptr = out + out_used;
size_t out_left = cap - out_used;
if (iconv(conv->cd, NULL, NULL, &out_ptr, &out_left) == (size_t)-1) {
if (errno != E2BIG) {
if (err_out)
*err_out = errno;
charset_conversion_reset(conv);
free(out);
return NULL;
}
out_used = (size_t)(out_ptr - out);
if (!grow_charset_buffer(&out, &cap))
return NULL;
continue;
}
out_used = (size_t)(out_ptr - out);
break;
}
/* A successful iconv call may legitimately consume the whole buffer (output
exactly fills cap), leaving no room for the terminator: guarantee headroom
before the final write. */
if (out_used >= cap && !grow_charset_buffer(&out, &cap))
return NULL;
/* Defense in depth: a target charset that emits embedded NUL bytes would
truncate at the first NUL in the C-string wire helpers; fail cleanly
(validation already rejects such charsets up front). */
if (memchr(out, '\0', out_used) != NULL) {
if (err_out)
*err_out = EILSEQ;
charset_conversion_reset(conv);
free(out);
return NULL;
}
out[out_used] = '\0';
return out;
}
bool charset_wire_init_sender(const char* spec) {
charset_wire_free();
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
void* conv = charset_conversion_open(local, remote);
free(local);
free(remote);
if (!conv)
return false;
g_wire_conv = (CharsetConversion*)conv;
return true;
}
bool charset_wire_init_receiver(const char* spec, const char* server_spec) {
charset_wire_free();
if (!spec)
return true;
char* local;
char* remote;
if (charset_spec_parse(spec, &local, &remote) != 0)
return false;
/* The wire charset is the client spec's REMOTE half; the local charset is
* the client spec's LOCAL half unless the server was itself started with
* --iconv naming a different local charset (the server halves above never
* travel, so the server's own flag is the only way its local charset can
* differ from what the client assumed). */
const char* wire = remote;
const char* target_local = local;
char* server_local = NULL;
char* server_remote = NULL;
if (server_spec) {
if (charset_spec_parse(server_spec, &server_local, &server_remote) != 0) {
free(local);
free(remote);
return false;
}
target_local = server_local;
}
void* conv = charset_conversion_open(wire, target_local);
free(server_local);
free(server_remote);
free(local);
free(remote);
if (!conv)
return false;
g_wire_conv = (CharsetConversion*)conv;
return true;
}
void charset_wire_free(void) {
if (g_wire_conv) {
charset_conversion_close(g_wire_conv);
g_wire_conv = NULL;
}
}
bool charset_wire_active(void) {
return g_wire_conv != NULL;
}
char* charset_wire_apply(const char* path) {
if (!g_wire_conv)
return str_dup(path);
return charset_convert(g_wire_conv, path, NULL);
}
static void charset_convert_failure_log(const char* path) {
char* escaped = output_escape(path, false);
log_message(LOG_LEVEL_ERROR, "--iconv: cannot convert file name '%s' to the target charset",
escaped ? escaped : "<unprintable>");
free(escaped);
}
bool send_wire_str(int file_descriptor, const char* local_path) {
if (!g_wire_conv)
return send_str(file_descriptor, local_path);
char* wire = charset_wire_apply(local_path);
if (!wire) {
charset_convert_failure_log(local_path);
return false;
}
bool ok = send_str(file_descriptor, wire);
free(wire);
return ok;
}
char* receive_wire_str(int file_descriptor) {
char* raw = receive_str(file_descriptor);
if (!raw)
return NULL;
if (!g_wire_conv)
return raw;
char* local = charset_convert(g_wire_conv, raw, NULL);
if (!local) {
charset_convert_failure_log(raw);
free(raw);
return NULL;
}
free(raw);
return local;
}
+85
View File
@@ -0,0 +1,85 @@
#ifndef CHARSET_H
#define CHARSET_H
#include <stdbool.h>
#include <stddef.h>
/* --iconv=CONVERT_SPEC file-name charset conversion (rsync compatibility).
*
* CONVERT_SPEC is "LOCAL[,REMOTE]": LOCAL is the charset of our own file
* names, REMOTE is the charset of the remote side's file names and defaults
* to LOCAL when the comma half is omitted. The sender converts every local
* path from LOCAL to REMOTE before it goes on the wire; the receiver converts
* every received path back from REMOTE to LOCAL. A NULL/disabled spec means
* identity with zero overhead (the common path never consults iconv).
*
* All helpers are friendly to the strict cold path: the wire conversion state
* is process-global (one direction per process -- a client only sends, a
* server only receives) and is initialized once, before any path is
* serialized, so conversion compiles to a single non-NULL check when disabled.
*/
/* Parse CONVERT_SPEC into malloc'd LOCAL and REMOTE charset names (caller
* frees both). REMOTE is a separate copy of LOCAL when no comma is present.
* Returns 0 on success, -1 on a malformed spec (empty halves / missing value /
* allocation failure); nothing is allocated on the -1 path. Both output
* pointers are REQUIRED (non-NULL). */
int charset_spec_parse(const char* spec, char** local_out, char** remote_out);
/* True when a CONVERT_SPEC is well-formed AND its charsets are usable for this
* feature: each pair opens in a probe iconv_open in BOTH directions (a sender
* converts local->remote, the receiver converts remote->local) and converting
* a representative ASCII name emits no embedded NUL byte (a UTF-16-style NUL
* emitter would be silently truncated by the C-string wire helpers). A typo'd
* charset name is therefore rejected at startup, not mid-run. NULL (iconv
* disabled) is always valid. */
bool charset_spec_valid(const char* spec);
/* Probe a concrete from->to conversion pair without keeping the descriptor:
* both charsets open AND a representative ASCII name converts with no embedded
* NUL. Used for direction-specific validation (e.g. the receiver's exact
* wire->local direction including a server-side charset override). */
bool charset_spec_valid_direction(const char* from_charset, const char* to_charset);
bool charset_pair_valid(const char* local, const char* remote);
/* One-shot conversion of a NUL-terminated input to a malloc'd NUL-terminated
* result, or NULL on failure. On failure *err_out (when non-NULL) receives
* the iconv errno (EILSEQ/EINVAL = the input is not representable in the
* target charset). The caller must free the result. */
char* charset_convert(const void* conversion, const char* in, int* err_out);
/* Open a conversion descriptor for direction from_charset -> to_charset.
* Returns NULL (errno = EINVAL) when a charset name is unsupported. Freed
* with charset_conversion_close. */
void* charset_conversion_open(const char* from_charset, const char* to_charset);
void charset_conversion_close(void* conversion);
/* Process-wide wire conversion. charset_wire_init_sender (client side) opens
* LOCAL->REMOTE; charset_wire_init_receiver (server side) opens
* wire(REMOTE)->server-local. server_spec is the server's own --iconv, whose
* LOCAL half may override the local charset the client assumed; NULL reuses
* the client spec's LOCAL half. Both return false on an unsupported spec.
* The state is freed with charset_wire_free. */
bool charset_wire_init_sender(const char* spec);
bool charset_wire_init_receiver(const char* spec, const char* server_spec);
void charset_wire_free(void);
bool charset_wire_active(void);
/* Pre-ack receiver-direction sanity (see charset_wire_init_receiver): true
* when the exact wire->server-local conversion the receiver will use (client
* spec's REMOTE half into the server's own LOCAL half, or the client's LOCAL
* half when the server has no --iconv) opens and produces NUL-free output. */
bool charset_wire_receiver_spec_valid(const char* spec, const char* server_spec);
/* Convert a path across the wire in the process direction. Returns a malloc'd
* string, or NULL when the name cannot be represented in the target charset. */
char* charset_wire_apply(const char* path);
/* Convenience wire string I/O: encode+send_str / receive_str+decode. Both
* return false/NULL (logging a clear --iconv error) on conversion failure, so
* an unconvertible path FAILS the transfer cleanly instead of silently sending
* a mangled name. */
bool send_wire_str(int file_descriptor, const char* local_path);
char* receive_wire_str(int file_descriptor);
#endif
+65 -5
View File
@@ -6,6 +6,7 @@
#include <string.h>
#include "array_list.h"
#include "charset.h"
#include "chunk.h"
#include "compression.h"
#include "data.h"
@@ -63,9 +64,22 @@ void chunk_destroy(void* item) {
free(chunk);
}
/* --iconv: a chunk blob carries wire-charset path/target bytes. Encode the
* sender-side path (a no-op copy when iconv is disabled) so the blob is in the
* same charset as every other wire string. */
static char* chunk_encode_wire(const char* path) {
if (!charset_wire_active())
return str_dup(path);
return charset_wire_apply(path);
}
static unsigned long long per_file_serialize_size(File* file, bool use_metadata) {
unsigned long long size = sizeof(size_t);
size_t path_len = strlen(file_wire_path(file));
char* wire_path = chunk_encode_wire(file_wire_path(file));
if (!wire_path)
return 0;
size_t path_len = strlen(wire_path);
free(wire_path);
unsigned long long metadata_size =
use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0;
if ((unsigned long long)path_len > ULLONG_MAX - size)
@@ -94,7 +108,11 @@ static unsigned long long per_file_serialize_size(File* file, bool use_metadata)
size += file->data->size;
/* Symlink entries append the target string (length-prefixed). */
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
char* wire_target = chunk_encode_wire(file->symlink_target ? file->symlink_target : "");
if (!wire_target)
return 0;
size_t target_len = strlen(wire_target);
free(wire_target);
if (sizeof(size_t) > ULLONG_MAX - size)
return 0;
size += sizeof(size_t);
@@ -128,12 +146,17 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
char* data_pointer = data->data;
for (int i = 0; i < chunk->element_count; i++) {
File* file = chunk->items[i];
const char* wire_path = file_wire_path(file);
char* wire_path = chunk_encode_wire(file_wire_path(file));
if (wire_path == NULL) {
data_destroy(data);
return NULL;
}
size_t path_len = strlen(wire_path);
memcpy(data_pointer, &path_len, sizeof(size_t));
data_pointer += sizeof(size_t);
memcpy(data_pointer, wire_path, path_len);
data_pointer += path_len;
free(wire_path);
int entry_type = file->is_dir ? 1 : (file->is_symlink ? 2 : (file->is_special ? 3 : 0));
memcpy(data_pointer, &entry_type, sizeof(int));
@@ -159,12 +182,18 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
data_pointer += file_data_size;
if (file->is_symlink) {
size_t target_len = file->symlink_target ? strlen(file->symlink_target) : 0;
char* wire_target = chunk_encode_wire(file->symlink_target ? file->symlink_target : "");
if (wire_target == NULL) {
data_destroy(data);
return NULL;
}
size_t target_len = strlen(wire_target);
memcpy(data_pointer, &target_len, sizeof(size_t));
data_pointer += sizeof(size_t);
if (target_len > 0)
memcpy(data_pointer, file->symlink_target, target_len);
memcpy(data_pointer, wire_target, target_len);
data_pointer += target_len;
free(wire_target);
}
}
return data;
@@ -222,6 +251,22 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
data_pointer += path_len;
remaining_size -= path_len;
/* --iconv: the blob holds the wire charset; translate it to the receiver's
local charset before validation and creation so the destination gets the
local name. A name that cannot be decoded fails the file cleanly. */
if (charset_wire_active()) {
char* local_path = charset_wire_apply(path);
free(path);
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;
}
path = local_path;
path_len = strlen(path);
}
if (path_len == 0 || has_path_traversal(path)) {
free(path);
array_list_delete(files);
@@ -392,6 +437,21 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
array_list_delete(files);
return NULL;
}
/* The symlink target also rides the wire charset; decode it to the local
charset like the path (a target is a path). */
if (charset_wire_active()) {
char* local_target = charset_wire_apply(target);
free(target);
if (local_target == NULL) {
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;
}
target = local_target;
}
file->symlink_target = target;
data_pointer += target_len;
remaining_size -= target_len;
+39 -3
View File
@@ -1,4 +1,5 @@
#include "config.h"
#include "charset.h"
#include "chmod.h"
#include "credentials.h"
#include "daemon_conf.h"
@@ -42,6 +43,7 @@ static void config_set_defaults(Config* config) {
config->auth_user = NULL;
config->auth_password_hash = NULL;
config->password_file = NULL;
config->iconv_spec = NULL;
config->fastsync_server_path = NULL;
config->exclude_patterns = NULL;
config->exclude_count = 0;
@@ -176,6 +178,9 @@ static void config_set_defaults(Config* config) {
config->use_xattrs = false;
config->fake_super = false;
config->trust_sender = false;
config->stop_after_mins = 0;
config->stop_at = 0;
config->stop_at_set = false;
}
static bool valid_wire_bool(int value) {
@@ -242,7 +247,13 @@ static bool validate_received_config(const Config* config) {
config->max_delete >= -1 && config->skip_compress_count >= 0 &&
config->skip_compress_count <= 10000 && config->max_alloc > 0 &&
(!config->chmod_spec || !*config->chmod_spec ||
chmod_apply(0, config->chmod_spec, &(mode_t){0}));
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));
}
Config* config_create(void) {
@@ -619,6 +630,7 @@ void config_delete(Config* config) {
free(config->auth_user);
free(config->auth_password_hash);
free(config->password_file);
free(config->iconv_spec);
free(config->fastsync_server_path);
for (int i = 0; i < config->exclude_count; i++)
free(config->exclude_patterns[i]);
@@ -1144,6 +1156,29 @@ static bool receive_daemon_auth(int fd, Config* c) {
return true;
}
/* --iconv CONVERT_SPEC (protocol 2.16.0). Trailing string on the config frame,
* sent after the Wave A/B daemon-auth block and before the ack, so the
* receiver knows the wire charset before the first file name arrives. The full
* spec travels (LOCAL,REMOTE) and each end derives its own LOCAL and the wire
* (REMOTE) charset symmetrically; an unset spec is serialized as "" and
* canonicalized back to NULL on receive. */
static bool send_iconv_spec(int fd, const Config* c) {
return send_str(fd, c->iconv_spec ? c->iconv_spec : "");
}
static bool receive_iconv_spec(int fd, Config* c) {
char* spec = receive_str(fd);
if (!spec)
return false;
if (*spec == '\0') {
free(spec);
c->iconv_spec = NULL;
return true;
}
c->iconv_spec = spec;
return true;
}
bool config_send(int file_descriptor, const Config* config) {
protocol_session_set_max_alloc(NULL, config->max_alloc);
if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
@@ -1156,7 +1191,8 @@ bool config_send(int file_descriptor, const Config* config) {
!send_metadata_times_options(file_descriptor, config) ||
!send_symlink_trust_options(file_descriptor, config) ||
!send_phase4_xattr_options(file_descriptor, config) ||
!send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config))
!send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config) ||
!send_iconv_spec(file_descriptor, config))
return false;
Status status;
if (!receive_status(file_descriptor, &status))
@@ -1198,7 +1234,7 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
!receive_symlink_trust_options(file_descriptor, config) ||
!receive_phase4_xattr_options(file_descriptor, config) ||
!receive_daemon_module(file_descriptor, config) ||
!receive_daemon_auth(file_descriptor, config))
!receive_daemon_auth(file_descriptor, config) || !receive_iconv_spec(file_descriptor, config))
goto error;
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
strcmp(config->compress_choice, "none") != 0) {
+39 -2
View File
@@ -6,6 +6,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
@@ -110,6 +111,17 @@ typedef struct Config {
* populate auth_user/auth_password_hash before connecting). */
char* password_file;
char* fastsync_server_path;
/* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the
* charset of FILE NAMES at the wire boundary. CONVERT_SPEC is
* "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is
* the remote side's charset and defaults to LOCAL. The sender converts
* every path LOCAL->REMOTE before transmitting it; the receiver converts
* every received path back REMOTE->LOCAL before creating/writing it. The
* FULL SPEC crosses the wire as a trailing config-frame string so each end
* derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL
* (or "") means no conversion: identity with zero overhead. See charset.c
* and the PROTOCOL_VERSION note below. */
char* iconv_spec;
char** exclude_patterns;
int exclude_count;
char** include_patterns;
@@ -446,6 +458,18 @@ typedef struct Config {
* authorized root (see the phase-5 notes in RSYNC_COMPAT.md). Off by
* default; only relaxes validation when explicitly requested. */
bool trust_sender;
// Phase 6: --stop-after / --stop-at
/* Client-only sender-side transfer stop deadlines. --stop-after=MINS stops
* the transfer after a number of elapsed minutes (checked against
* CLOCK_MONOTONIC so clock changes do not skew it); --stop-at=TIME stops at
* an absolute wall-clock time (HH:MM, HH:MM:SS, or now+N[smhd]). At the
* deadline the run stops elegantly at the next chunk/file boundary and the
* completion tail still runs (exit 0). Both are LOCAL to the sending
* process and are NEVER serialized into the config frame. */
int stop_after_mins; /* --stop-after=MINS minutes; 0 when unset */
time_t stop_at; /* --stop-at=... absolute wall-clock deadline */
bool stop_at_set; /* true when --stop-at was given */
} Config;
/* Phase 5 (remote-option wave): 2.13.0 -> 2.14.0.
@@ -493,8 +517,21 @@ typedef struct Config {
* reads that frame right after the ack (client_send.c) -- symmetric
* server->client in every build, so the strict same-version handshake keeps the
* two peers in lockstep and nothing can desynchronize. The --stdio SSH path
* sends/reads no MOTD at all. */
#define PROTOCOL_VERSION "2.15.0"
* sends/reads no MOTD at all.
*
* --iconv Wave (P6): 2.15.0 -> 2.16.0.
*
* WHY the bump, grounded in the wire: the --iconv feature adds a serialized
* field to the binary config frame. The client sends the full CONVERT_SPEC
* (Config->iconv_spec) as a new trailing string AFTER the Wave A/B daemon-auth
* block (in config_send/config_receive), so the receiver knows the wire charset
* (the REMOTE half) before the first file name arrives. Any config-frame
* layout change must bump the protocol version: a peer that does not parse the
* new trailing bytes would desynchronize on the frame boundary, and the strict
* same-version handshake (config_receive rejects a mismatched version before
* parsing anything else) is what keeps a 2.16 client and a 2.15 server from
* ever reaching that state. */
#define PROTOCOL_VERSION "2.16.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
#define MAX_BASIS_DIRS 64
+10 -9
View File
@@ -10,6 +10,7 @@
#include <unistd.h>
#include "array_list.h"
#include "charset.h"
#include "chmod.h"
#include "compression.h"
#include "config.h"
@@ -1555,7 +1556,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return NULL;
}
*skipped = false;
char* check_path = receive_str(fd);
char* check_path = receive_wire_str(fd);
if (check_path == NULL) {
return NULL;
}
@@ -2082,7 +2083,7 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
}
File* file_receive(const Config* config, int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2142,7 +2143,7 @@ File* file_receive(const Config* config, int file_descriptor) {
traversal), and the created File is routed through the regular store_file
sink so single-threaded and -m receivers handle directories identically. */
File* file_receive_directory(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2169,7 +2170,7 @@ File* file_receive_directory(int file_descriptor) {
member. All paths are validated like every other received path (non-empty,
relative, no traversal). */
File* file_receive_hardlink(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2186,7 +2187,7 @@ File* file_receive_hardlink(int file_descriptor) {
free(path);
return NULL;
}
char* target = receive_str(file_descriptor);
char* target = receive_wire_str(file_descriptor);
if (!target) {
free(path);
return NULL;
@@ -2219,7 +2220,7 @@ File* file_receive_hardlink(int file_descriptor) {
routed through the regular store_file sink, which creates the link beneath
the receive root (unmungeing the target first). */
File* file_receive_symlink(int file_descriptor, const Config* config) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2231,7 +2232,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
send_status(file_descriptor, STATUS_ERROR);
return NULL;
}
char* target = receive_str(file_descriptor);
char* target = receive_wire_str(file_descriptor);
if (!target) {
free(path);
return NULL;
@@ -2274,7 +2275,7 @@ File* file_receive_symlink(int file_descriptor, const Config* config) {
* confined). rdev is validated here (non-negative, range-checked) so a bogus
* value cannot drive a dangerous node on the receiver. */
File* file_receive_special(int file_descriptor) {
char* path = receive_str(file_descriptor);
char* path = receive_wire_str(file_descriptor);
if (path == NULL)
return NULL;
if (path[0] == '\0' || (!file_get_trust_sender() && has_path_traversal(path))) {
@@ -2354,7 +2355,7 @@ static bool receive_manifest_section(int fd, ArrayList* list, size_t* manifest_b
return false;
}
for (int i = 0; i < count; i++) {
char* s = receive_str(fd);
char* s = receive_wire_str(fd);
size_t entry_size = s ? strlen(s) : 0;
if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) ||
entry_size > MAX_MANIFEST_BYTES - *manifest_bytes ||
+4 -3
View File
@@ -10,6 +10,7 @@
#include <time.h>
#include <unistd.h>
#include "charset.h"
#include "compression.h"
#include "data.h"
#include "file.h"
@@ -27,7 +28,7 @@ bool file_send_special(const File* file, int file_descriptor, bool use_metadata)
return false;
if (!send_status(file_descriptor, STATUS_SPECIAL))
return false;
if (!send_str(file_descriptor, file_wire_path(file)))
if (!send_wire_str(file_descriptor, file_wire_path(file)))
return false;
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false;
@@ -61,7 +62,7 @@ bool file_send_single_calls_with_skip(File* file, int file_descriptor, bool use_
}
data_to_send = compressed_data;
}
if (send_path && !send_str(file_descriptor, file_wire_path(file))) {
if (send_path && !send_wire_str(file_descriptor, file_wire_path(file))) {
data_destroy(compressed_data);
return false;
}
@@ -97,7 +98,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
send_path, skip_suffixes, skip_count,
compression_threads, send_xattrs);
if (send_path && !send_str(file_descriptor, file_wire_path(file)))
if (send_path && !send_wire_str(file_descriptor, file_wire_path(file)))
return false;
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false;
+1
View File
@@ -31,6 +31,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
context->scan_had_io_error = false;
context->remove_source_files = NULL;
context->early_delete = false;
context->scan_stopped_early = false;
context->total_files = 0;
context->progress_bytes = 0;
context->total_bytes = 0;
+11
View File
@@ -10,6 +10,7 @@
#include "protocol.h"
#include "queue.h"
#include "receiver.h"
#include "stop_condition.h"
#include <openssl/ssl.h>
typedef struct {
@@ -56,6 +57,16 @@ typedef struct {
bool sender_done;
atomic_bool cancelled;
ProtocolSession allocation_session;
/* Phase 6: client-only sender stop deadline, computed once before the worker
* threads start and shared read-only by the scanner and the sender thread. */
StopCondition stop_condition;
/* Phase 6: set when the scanner/sender reached the stop deadline before the
* scan (and thus the keep-set manifest) completed naturally. When true the
* completion tail must NOT transmit the partial manifest, or the receiver
* would delete unscanned source mirrors. Written by the sender thread
* before it reads the manifest, so no additional synchronization is needed
* to suppress the manifest. */
bool scan_stopped_early;
} PipelineContextSender;
typedef struct PipelineContextReceiver {
+157
View File
@@ -0,0 +1,157 @@
#include "stop_condition.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
/* Parse a strictly positive decimal integer: only ASCII digits, no leading
* whitespace, sign or trailing garbage. */
static bool parse_positive_minutes(const char* value, long* out) {
if (!value || *value == '\0')
return false;
if (*value < '0' || *value > '9')
return false;
long v = 0;
for (const char* p = value; *p != '\0'; p++) {
if (*p < '0' || *p > '9')
return false;
int digit = *p - '0';
if (v > (LONG_MAX - digit) / 10)
return false;
v = v * 10 + digit;
}
if (v <= 0 || v > INT_MAX)
return false;
*out = v;
return true;
}
bool stop_parse_after_minutes(const char* value, int* out_minutes) {
if (!out_minutes)
return false;
long minutes = 0;
if (!parse_positive_minutes(value, &minutes))
return false;
*out_minutes = (int)minutes;
return true;
}
/* Two consecutive ASCII digits -> 0..99. */
static bool parse_two_digits(const char* s, int* out) {
if (s[0] < '0' || s[0] > '9' || s[1] < '0' || s[1] > '9')
return false;
*out = (s[0] - '0') * 10 + (s[1] - '0');
return true;
}
bool stop_parse_at_time(const char* value, time_t now, time_t* out_deadline) {
if (!value || !out_deadline)
return false;
/* now+N[smhd]: N whole units from the current wall clock. */
if (strncmp(value, "now+", 4) == 0) {
const char* p = value + 4;
/* The count must be a bare non-negative digit run: reject leading
whitespace ('now+ 5s') and a leading sign ('now++5s'). */
if (*p < '0' || *p > '9')
return false;
errno = 0;
char* end = NULL;
long amount = strtol(p, &end, 10);
if (errno != 0 || end == p || amount < 0)
return false;
long unit_seconds;
switch (*end) {
case 's':
unit_seconds = 1;
break;
case 'm':
unit_seconds = 60;
break;
case 'h':
unit_seconds = 3600;
break;
case 'd':
unit_seconds = 86400;
break;
default:
return false;
}
if (end[1] != '\0')
return false;
if (amount > LONG_MAX / unit_seconds)
return false;
long long delta = (long long)amount * unit_seconds;
/* Guard against signed overflow of now + delta. */
if ((long long)now > 0 && delta > (long long)LLONG_MAX - (long long)now)
return false;
if ((long long)now < 0 && delta < (long long)LLONG_MIN - (long long)now)
return false;
*out_deadline = now + (time_t)delta;
return true;
}
/* HH:MM or HH:MM:SS on the current local day. */
size_t len = strlen(value);
if (len != 5 && len != 8)
return false;
if (value[2] != ':' || (len == 8 && value[5] != ':'))
return false;
int hh, mm, ss = 0;
if (!parse_two_digits(value, &hh) || !parse_two_digits(value + 3, &mm))
return false;
if (len == 8 && !parse_two_digits(value + 6, &ss))
return false;
if (hh > 23 || mm > 59 || ss > 59)
return false;
struct tm today;
if (!localtime_r(&now, &today))
return false;
today.tm_hour = hh;
today.tm_min = mm;
today.tm_sec = ss;
today.tm_isdst = -1;
time_t deadline = mktime(&today);
if (deadline == (time_t)-1)
return false;
*out_deadline = deadline;
return true;
}
StopCondition stop_condition_make(bool has_after, int after_minutes, bool has_at, time_t at_time,
struct timespec now_mono) {
StopCondition condition;
condition.has_monotonic = false;
condition.monotonic_deadline.tv_sec = 0;
condition.monotonic_deadline.tv_nsec = 0;
condition.has_wall = false;
condition.wall_deadline = 0;
if (has_after && after_minutes > 0) {
condition.has_monotonic = true;
condition.monotonic_deadline.tv_sec = now_mono.tv_sec + (time_t)after_minutes * 60;
condition.monotonic_deadline.tv_nsec = now_mono.tv_nsec;
}
if (has_at) {
condition.has_wall = true;
condition.wall_deadline = at_time;
}
return condition;
}
bool stop_condition_reached(const StopCondition* condition) {
if (!condition)
return false;
if (condition->has_wall && time(NULL) >= condition->wall_deadline)
return true;
if (condition->has_monotonic) {
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
return false;
if (now.tv_sec > condition->monotonic_deadline.tv_sec ||
(now.tv_sec == condition->monotonic_deadline.tv_sec &&
now.tv_nsec >= condition->monotonic_deadline.tv_nsec))
return true;
}
return false;
}
+46
View File
@@ -0,0 +1,46 @@
#ifndef STOP_CONDITION_H
#define STOP_CONDITION_H
#include <stdbool.h>
#include <time.h>
/* Client-only transfer stop conditions (--stop-after=MINS / --stop-at=TIME).
* Both are local sender-side deadlines: they are never serialized into the
* config frame and never bump PROTOCOL_VERSION. A transfer checks the
* condition at natural chunk/file boundaries and, once reached, stops
* elegantly (everything already sent is finalized normally, exit 0).
*
* A condition combines an optional CLOCK_MONOTONIC instant (the relative
* --stop-after duration, immune to wall-clock changes) with an optional
* wall-clock instant (the absolute --stop-at form). Either one being reached
* ends the transfer. */
typedef struct StopCondition {
bool has_monotonic;
struct timespec monotonic_deadline;
bool has_wall;
time_t wall_deadline;
} StopCondition;
/* Parse --stop-after=MINS: a positive integer count of minutes. Zero,
* negative, empty and non-numeric values are rejected. Returns true when
* accepted and stores the value in *out_minutes. */
bool stop_parse_after_minutes(const char* value, int* out_minutes);
/* Parse --stop-at=TIME. Accepted forms are HH:MM, HH:MM:SS and
* now+N[smhd] (seconds/minutes/hours/days from now). The absolute forms are
* resolved against `now` (local wall clock) and written to *out_deadline; a
* time already in the past yields a deadline <= now ("stop immediately").
* Returns false on any malformed value. */
bool stop_parse_at_time(const char* value, time_t now, time_t* out_deadline);
/* Build the runtime condition at transfer start. after_minutes is the
* relative --stop-after duration (<= 0 disables it); at_time is the absolute
* --stop-at deadline (only consulted when has_at is true); now_mono is the
* CLOCK_MONOTONIC reading at start. */
StopCondition stop_condition_make(bool has_after, int after_minutes, bool has_at, time_t at_time,
struct timespec now_mono);
/* True once either deadline has passed (wall clock first, then monotonic). */
bool stop_condition_reached(const StopCondition* condition);
#endif
+250
View File
@@ -0,0 +1,250 @@
"""--iconv=CONVERT_SPEC file-NAME charset conversion integration tests.
The client converts every source file name from LOCAL to REMOTE before it goes
on the wire, and the receiver converts it back from REMOTE to LOCAL, so a
source tree using one charset can be written into a destination tree using
another (rsync compatibility; content bytes are never touched).
"""
import os
import shutil
import pytest
from common import TEST_DATA_DIR, run_client, clean_dir, ServerManager
LATIN1_NAME = b"caf\xe9.txt"
UTF8_NAME = "caf\u00e9.txt".encode("utf-8")
def _make(tag):
source = os.path.join(TEST_DATA_DIR, f"iconv_{tag}_src")
dest = os.path.join(TEST_DATA_DIR, f"iconv_{tag}_dst")
clean_dir(source)
shutil.rmtree(dest, ignore_errors=True)
# The destination ROOT must pre-exist on the receiver (the --mkpath contract:
# without --mkpath the server requires the root directory to exist).
os.makedirs(dest, exist_ok=True)
return source, dest
def _place_bytes(root, name_bytes, data=b"latin1 payload\n"):
full = os.path.join(os.fsencode(root), name_bytes)
os.makedirs(os.path.dirname(full), exist_ok=True)
with open(full, "wb") as fh:
fh.write(data)
return full
def _dest_file(source, dest, name):
base = os.path.join(dest, os.path.abspath(source).lstrip(os.sep))
return os.path.join(os.fsencode(base), name)
@pytest.mark.ci
def test_iconv_latin1_roundtrip(shared_server):
"""A source file whose name is ISO-8859-1 bytes is transferred with
--iconv=iso-8859-1,utf-8 and lands on the destination with the ORIGINAL
latin1 name (the wire carried it as UTF-8)."""
source, dest = _make("latin1")
_place_bytes(source, LATIN1_NAME)
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8"], port=shared_server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
dst = _dest_file(source, dest, LATIN1_NAME)
assert os.path.exists(dst), f"dest latin1-named file not found under {dest}"
@pytest.mark.ci
def test_iconv_to_utf8_on_wire(shared_server):
"""--iconv=utf-8 (single, identity both ways) on an ascii filename transfers
cleanly with no error."""
source, dest = _make("utf8")
src_path = os.path.join(source, "plain.txt")
with open(src_path, "wb") as fh:
fh.write(b"identity\n")
result, _ = run_client(source, dest, flags=["--iconv=utf-8"], port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
dst = _dest_file(source, dest, os.fsencode("plain.txt"))
assert os.path.exists(dst)
@pytest.mark.ci
def test_iconv_passthrough_identity(shared_server):
"""No --iconv flag: the transfer is unchanged (regression guard -- the common
path must not go through iconv at all)."""
source, dest = _make("identity")
for name, data in (("a.txt", b"aaa\n"), ("sub/b.txt", b"bbb\n")):
p = os.path.join(source, name)
os.makedirs(os.path.dirname(p), exist_ok=True)
with open(p, "wb") as fh:
fh.write(data)
result, _ = run_client(source, dest, port=shared_server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
for name in ("a.txt", "sub/b.txt"):
assert os.path.exists(_dest_file(source, dest, os.fsencode(name)))
@pytest.mark.ci
def test_iconv_receiver_own_charset(shared_server):
"""A dedicated server started with its OWN --iconv converts received names
to ITS charset: the source holds a latin1-named file, the wire carries it
as UTF-8 (from the client's spec), and the receiver re-decodes it to UTF-8
on disk. This discriminates a real wire conversion from a no-op passthrough
(a latin1 byte sequence is not valid UTF-8, so the receiver decoding it as
UTF-8 would fail the transfer)."""
with ServerManager() as server:
server.start(extra_args=["--iconv=utf-8"])
source, dest = _make("recv_charset")
_place_bytes(source, LATIN1_NAME)
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8"], port=server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
dst = _dest_file(source, dest, UTF8_NAME)
assert os.path.exists(dst), f"dest UTF-8-named file not found under {dest}"
@pytest.mark.ci
def test_iconv_invalid_charset_rejected(shared_server):
"""An unsupported charset name is rejected at startup with a nonzero exit."""
source, dest = _make("badcharset")
src_path = os.path.join(source, "f.txt")
with open(src_path, "wb") as fh:
fh.write(b"x")
result, _ = run_client(
source, dest, flags=["--iconv=no-such-charset,utf-8"], port=shared_server.port
)
assert result.returncode != 0
@pytest.mark.ci
def test_iconv_garbage_spec_rejected(shared_server):
"""A malformed CONVERT_SPEC is rejected at startup with a nonzero exit."""
source, dest = _make("garbage")
src_path = os.path.join(source, "f.txt")
with open(src_path, "wb") as fh:
fh.write(b"x")
result, _ = run_client(source, dest, flags=["--iconv=,,,"], port=shared_server.port)
assert result.returncode != 0
@pytest.mark.ci
def test_iconv_expanding_name_growth(shared_server):
"""A long latin1 name whose UTF-8 encoding expands past the initial output
buffer exercises the E2BIG growth path in charset_convert (each high-bit
latin1 byte doubles in UTF-8), and must land unchanged on the destination."""
source, dest = _make("growth")
name_bytes = b"a" * 40 + bytes(range(0x80, 0x80 + 40)) + b".txt"
_place_bytes(source, name_bytes, data=b"growth\n")
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8"], port=shared_server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
assert os.path.exists(_dest_file(source, dest, name_bytes))
def test_iconv_symlink_path_and_target(shared_server):
"""A latin1-named symlink pointing at a latin1-named target survives the
transfer: both the link name and the link target are wire-converted and
re-decoded on the destination (-l preserves links)."""
source, dest = _make("symlink")
target = b"target\xe9.dat"
_place_bytes(source, target, data=b"t\n")
os.symlink(target, os.path.join(os.fsencode(source), b"link\xe9"))
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8", "--links"], port=shared_server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
dst_target = _dest_file(source, dest, target)
dst_link = _dest_file(source, dest, b"link\xe9")
assert os.path.exists(dst_target), "dest latin1 target file missing"
assert os.path.islink(dst_link), "dest latin1 symlink missing"
assert os.readlink(dst_link) == target, "symlink target not preserved/decoded"
with open(dst_link, "rb") as fh:
assert fh.read() == b"t\n"
def test_iconv_hardlink_path_and_target(shared_server):
"""A latin1-named hard-linked pair is preserved: -H transmits later group
members as a path+target link to the first member, so both the member name
and the target wire-convert (the two destination names must stay one
inode)."""
source, dest = _make("hardlink")
a = b"hl_a\xe9.txt"
b = b"hl_b\xe9.txt"
src_a = os.path.join(os.fsencode(source), a)
with open(src_a, "wb") as fh:
fh.write(b"shared\n")
os.link(src_a, os.path.join(os.fsencode(source), b))
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8", "--hard-links"],
port=shared_server.port,
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
dst_a = _dest_file(source, dest, a)
dst_b = _dest_file(source, dest, b)
assert os.path.exists(dst_a) and os.path.exists(dst_b)
assert os.stat(dst_a).st_ino == os.stat(dst_b).st_ino, \
"hard-link relationship not preserved across the transfer"
def test_iconv_delete_manifest_consistent(shared_server):
"""Combining --iconv with --delete: the delete manifest's keep-set paths are
wire-converted on send and disk-converted on receive, so the receiver's
delete walker compares like with like and removes exactly the missing
latin1-named file (never a wrong-named mirror)."""
source, dest = _make("delete")
keep = b"keep\xe9.txt"
gone = b"gone\xe9.txt"
_place_bytes(source, keep, data=b"k\n")
_place_bytes(source, gone, data=b"g\n")
with ServerManager() as server:
server.start(extra_args=["--allow-delete"])
flags = ["--iconv=iso-8859-1,utf-8"]
result, _ = run_client(source, dest, flags=flags, port=server.port)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
assert os.path.exists(_dest_file(source, dest, keep))
assert os.path.exists(_dest_file(source, dest, gone))
os.remove(os.path.join(os.fsencode(source), gone))
result, _ = run_client(
source, dest, flags=flags + ["--delete"], port=server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
assert os.path.exists(_dest_file(source, dest, keep)), "kept file deleted"
assert not os.path.exists(_dest_file(source, dest, gone)), \
"missing file was not deleted"
def test_iconv_chunk_serialization_blob(shared_server):
"""-s (chunk serialization) embeds paths and symlink targets inside the
serialized chunk blob rather than as separate frames; a latin1 name must
still wire-convert and re-decoded on the destination."""
source, dest = _make("chunk")
name = b"\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9\xe9.txt"
_place_bytes(source, name, data=b"blob\n")
result, _ = run_client(
source, dest, flags=["--iconv=iso-8859-1,utf-8", "-s"], port=shared_server.port
)
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
assert os.path.exists(_dest_file(source, dest, name))
+241
View File
@@ -0,0 +1,241 @@
"""--stop-after / --stop-at deadline-stop integration tests.
These cover the client-only sender stop conditions: --stop-after=MINS stops
after N elapsed minutes, --stop-at=HH:MM[:SS] or now+N[smhd] stops at an
absolute (or relative) wall-clock time. A reached deadline ends the transfer
elegantly at the next chunk/file boundary -- whatever was already transferred is
kept, the completion tail still runs, and the exit code is 0 (like rsync's
clean "stopped early" behavior). Malformed values are rejected up front.
"""
import filecmp
import os
import shutil
import time
import pytest
from common import (
TEST_DATA_DIR,
run_client,
clean_dir,
get_dest_received_dir,
verify_transfer,
)
def _make(self_prefix):
source = os.path.join(TEST_DATA_DIR, f"stop_{self_prefix}_src")
dest = os.path.join(TEST_DATA_DIR, f"stop_{self_prefix}_dst")
clean_dir(source)
shutil.rmtree(dest, ignore_errors=True)
os.makedirs(dest)
return source, dest
def _received_files(root):
"""All files under `root`, relative paths."""
if not os.path.isdir(root):
return []
return [
os.path.relpath(os.path.join(dirpath, name), root)
for dirpath, _, names in os.walk(root)
for name in names
]
def _seed_source(source):
"""Create a handful of regular and nested files."""
files = {
"small.txt": b"hello world\n",
"medium.txt": b"the quick brown fox jumps over the lazy dog\n" * 400,
"binary.bin": bytes(range(256)) * 100,
"nested/deep.txt": b"deeply nested file\n",
"nested/another.txt": b"another nested file\n" * 40,
}
for rel, content in files.items():
path = os.path.join(source, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(content)
def _seed_many(source, count=40, size=32 * 1024):
"""Create `count` same-size regular files (enough to span several chunks)."""
blob = os.urandom(size)
for i in range(count):
with open(os.path.join(source, f"f{i:04d}.dat"), "wb") as fh:
fh.write(blob)
def _seed_dest_by_transfer(source, dest, port, extra=None):
"""Do a plain full transfer source->dest so dest exactly mirrors source."""
run_client(source, dest, flags=(extra or []), port=port)
def _received_subset_matches(source, received):
"""Every file under `received` exists under `source` with identical bytes."""
if not os.path.isdir(received):
return not _received_files(received)
rels = _received_files(received)
for rel in rels:
src = os.path.join(source, rel)
dst = os.path.join(received, rel)
if not os.path.isfile(src) or not filecmp.cmp(src, dst, shallow=False):
return False
return True
class TestStopAfter:
@pytest.mark.ci
def test_stop_after_within_window(self, shared_server):
"""A --stop-after set well past the run's duration lets it finish fully."""
source, dest = _make("within")
_seed_source(source)
result, _ = run_client(source, dest, flags=["--stop-after=60"],
port=shared_server.port)
assert result.returncode == 0, \
f"--stop-after full run failed: {(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"full transfer mismatch: missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_stop_after_rejects_nonpositive(self, shared_server):
"""0 and negative minutes are invalid (must be a positive integer)."""
source, dest = _make("reject")
_seed_source(source)
for bad in ("0", "-1"):
result, _ = run_client(source, dest, flags=[f"--stop-after={bad}"],
port=shared_server.port)
assert result.returncode != 0, f"--stop-after={bad} should be rejected"
class TestStopAt:
@pytest.mark.ci
def test_stop_at_past(self, shared_server):
"""A --stop-at already in the past stops the transfer immediately but
cleanly (exit 0, nothing transferred)."""
source, dest = _make("past")
_seed_source(source)
# Use a same-day HH:MM two minutes in the past when that cannot roll
# over into the previous day (which would parse as a FUTURE time today);
# otherwise fall back to now+0s which is deterministically immediate.
lt = time.localtime()
if lt.tm_hour * 60 + lt.tm_min >= 3:
past = time.localtime(time.time() - 120)
stop_value = f"{past.tm_hour:02d}:{past.tm_min:02d}"
else:
stop_value = "now+0s"
result, _ = run_client(source, dest, flags=[f"--stop-at={stop_value}"],
port=shared_server.port)
assert result.returncode == 0, \
f"--stop-at past run failed (rc {result.returncode}): " \
f"{(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
assert _received_files(received) == [], \
f"expected nothing transferred, got {_received_files(received)}"
@pytest.mark.ci
def test_stop_at_now_plus_stops_immediately(self, shared_server):
"""now+0s resolves to the current instant, so the transfer stops at once."""
source, dest = _make("nowplus")
_seed_source(source)
result, _ = run_client(source, dest, flags=["--stop-at=now+0s"],
port=shared_server.port)
assert result.returncode == 0, \
f"--stop-at=now+0s should stop cleanly: " \
f"{(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
assert _received_files(received) == [], \
f"expected nothing transferred, got {_received_files(received)}"
@pytest.mark.ci
def test_stop_rejects_garbage(self, shared_server):
"""Malformed --stop-at/--stop-after values are rejected up front."""
source, dest = _make("garbage")
_seed_source(source)
for flag in ("--stop-after=abc", "--stop-at=12:99", "--stop-at=12",
"--stop-at=now+5x", "--stop-at=now-5s"):
result, _ = run_client(source, dest, flags=[flag],
port=shared_server.port)
assert result.returncode != 0, f"{flag} should be rejected"
class TestStopPartial:
"""A genuine mid-transfer stop leaves a valid, strict non-empty prefix."""
@pytest.mark.ci
def test_stop_mid_transfer_leaves_valid_partial(self, shared_server):
"""With --bwlimit a real deadline cuts the transfer mid-way: what WAS
transferred is byte-identical, not everything is transferred, and the
run returns 0 without corrupting any file."""
source, dest = _make("partial")
_seed_many(source, count=60, size=32 * 1024)
flags = ["--chunk-size", "262144", "--bwlimit", "100", "--stop-at=now+3s"]
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"mid-transfer stop failed (rc {result.returncode}): " \
f"{(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
got = _received_files(received)
assert len(got) > 0, "expected an early stop to still transfer a prefix"
assert len(got) < 60, \
f"expected a PARTIAL transfer (all 60 arrived): stopped too late"
assert _received_subset_matches(source, received), \
f"received files are not a byte-identical subset of the source"
class TestStopDelete:
"""--delete must never wipe the destination when the scan is cut short."""
def _seed(self, prefix, port, many=False):
source, dest = _make(prefix)
if many:
_seed_many(source, count=40, size=96 * 1024)
else:
_seed_source(source)
_seed_dest_by_transfer(source, dest, port)
return source, dest
@pytest.mark.ci
def test_stop_delete_immediate_preserves_source_mirrors(self, shared_server):
"""Immediate stop + --delete: the incomplete/empty keep-set must NOT
delete the seeded source mirrors (returncode 0, files survive)."""
source, dest = self._seed("del_imm", shared_server.port)
result, _ = run_client(source, dest, flags=["--delete", "--stop-at=now+0s"],
port=shared_server.port)
assert result.returncode == 0, \
f"--delete immediate stop failed: {(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"--delete wiped source mirrors: missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_stop_delete_midscan_preserves_source_mirrors(self, shared_server):
"""A mid-scan stop + --delete must suppress the partial keep-set so all
seeded source mirrors survive."""
source, dest = self._seed("del_mid", shared_server.port, many=True)
flags = ["--delete", "--chunk-size", "262144", "--bwlimit", "300", "--stop-at=now+3s"]
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
assert result.returncode == 0, \
f"--delete mid-scan stop failed: {(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"--delete mid-scan wiped source mirrors: missing={missing} mismatches={mismatches}"
@pytest.mark.ci
def test_stop_delete_multithreaded_preserves_source_mirrors(self, shared_server):
"""-m immediate stop + --delete: the completion tail must not read the
still-appendable manifest (no race) and must not delete the mirrors."""
source, dest = self._seed("del_mt", shared_server.port, many=True)
result, _ = run_client(source, dest, flags=["-m", "--delete", "--stop-at=now+0s"],
port=shared_server.port)
assert result.returncode == 0, \
f"-m --delete immediate stop failed: {(result.stderr or result.stdout)[:400]}"
received = get_dest_received_dir(dest, source)
mismatches, missing = verify_transfer(source, received)
assert not mismatches and not missing, \
f"-m --delete wiped source mirrors: missing={missing} mismatches={mismatches}"
+4
View File
@@ -14,6 +14,7 @@
#include "test_file_sendfile.h"
#include "test_fuzz_smoke.h"
#include "test_glob.h"
#include "test_iconv.h"
#include "test_log.h"
#include "test_metadata.h"
#include "test_motd.h"
@@ -27,6 +28,7 @@
#include "test_server_cli.h"
#include "test_shared_utils.h"
#include "test_stress.h"
#include "test_stop.h"
#include "test_transport_tcp.h"
#include "test_transport_ssh.h"
#include "test_transport_tls.h"
@@ -59,6 +61,7 @@ int main() {
RUN_TEST(test_protocol);
RUN_TEST(test_metadata);
RUN_TEST(test_glob);
RUN_TEST(test_iconv);
RUN_TEST(test_file);
RUN_TEST(test_trust_sender);
RUN_TEST(test_delay_updates);
@@ -67,6 +70,7 @@ int main() {
RUN_TEST(test_log);
RUN_TEST(test_robustness);
RUN_TEST(test_stress);
RUN_TEST(test_stop);
RUN_TEST(test_property);
RUN_TEST(test_transport_tcp);
RUN_TEST(test_transport_ssh);
+107
View File
@@ -1565,6 +1565,110 @@ static void test_config_local_only_fields_not_serialized() {
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
static void test_config_iconv_spec_wire_roundtrip() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("rel/path");
send_cfg->iconv_spec = str_dup("utf-8,iso-8859-1");
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv_cfg = config_receive(p[0]);
bool ok = recv_cfg != NULL && recv_cfg->iconv_spec != NULL &&
strcmp(recv_cfg->iconv_spec, "utf-8,iso-8859-1") == 0;
config_delete(recv_cfg);
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_config_iconv_spec_empty_canonicalizes_to_null() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("/dst");
/* iconv_spec left NULL -> serialized as "" -> received back as NULL. */
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
Config* recv_cfg = config_receive(p[0]);
bool ok = recv_cfg != NULL && recv_cfg->iconv_spec == NULL;
config_delete(recv_cfg);
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
static void test_config_receive_rejects_invalid_iconv_spec() {
Config* send_cfg = config_create();
EXPECT_NOT_NULL(send_cfg);
send_cfg->send_directory = str_dup("/src");
send_cfg->receive_root_directory = str_dup("/dst");
send_cfg->iconv_spec = str_dup("no-such-charset,utf-8");
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
/* A malformed/unsupported spec must be refused at the config handshake
(STATUS_ERROR makes config_send fail on the parent). */
Config* recv_cfg = config_receive(p[0]);
config_delete(recv_cfg);
close(p[0]);
_exit(recv_cfg ? 1 : 0);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = config_send(p[1], send_cfg);
int status;
waitpid(pid, &status, 0);
close(p[1]);
config_delete(send_cfg);
EXPECT_FALSE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
void test_config() {
test_config_lifecycle();
test_config_ssh_dest();
@@ -1608,6 +1712,9 @@ void test_config() {
test_config_module_wire_empty_canonicalizes_to_null();
test_config_daemon_auth_wire_roundtrip();
test_config_daemon_auth_wire_rejects_malformed();
test_config_iconv_spec_wire_roundtrip();
test_config_iconv_spec_empty_canonicalizes_to_null();
test_config_receive_rejects_invalid_iconv_spec();
test_config_receive_with_validate_rejects();
}
test_config_delete_timing_early_helper();
+217
View File
@@ -0,0 +1,217 @@
#include "test_iconv.h"
#include "charset.h"
#include "protocol.h"
#include "test_utils.h"
#include "utils.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
/* --- CONVERT_SPEC parsing ------------------------------------------------ */
static void test_iconv_spec_parse_split() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse("utf-8,iso-8859-1", &local, &remote), 0);
EXPECT_EQ_STR(local, "utf-8");
EXPECT_EQ_STR(remote, "iso-8859-1");
free(local);
free(remote);
}
static void test_iconv_spec_parse_single_defaults_to_local() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse("utf-8", &local, &remote), 0);
EXPECT_EQ_STR(local, "utf-8");
EXPECT_EQ_STR(remote, "utf-8");
free(local);
free(remote);
}
static void test_iconv_spec_parse_garbage() {
char* local = NULL;
char* remote = NULL;
EXPECT_EQ_INT(charset_spec_parse(NULL, &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse("", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse(",", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse("utf-8,", &local, &remote), -1);
EXPECT_EQ_INT(charset_spec_parse(",utf-8", &local, &remote), -1);
}
static void test_iconv_spec_valid() {
EXPECT_TRUE(charset_spec_valid(NULL));
EXPECT_TRUE(charset_spec_valid("utf-8"));
EXPECT_TRUE(charset_spec_valid("utf-8,iso-8859-1"));
EXPECT_TRUE(charset_spec_valid("iso-8859-1,ascii"));
EXPECT_FALSE(charset_spec_valid("no-such-charset,utf-8"));
EXPECT_FALSE(charset_spec_valid("utf-8,no-such-charset"));
EXPECT_FALSE(charset_spec_valid(",,,"));
EXPECT_FALSE(charset_spec_valid("utf-8,"));
/* A target charset whose conversion emits embedded NUL bytes would be
truncated by the C-string wire helpers; it must be rejected up front. */
EXPECT_FALSE(charset_spec_valid("utf-8,utf-16"));
EXPECT_FALSE(charset_spec_valid("utf-16"));
EXPECT_FALSE(charset_spec_valid("iso-8859-1,utf-16"));
}
/* --- one-shot conversion ------------------------------------------------ */
static void test_iconv_utf8_to_latin1() {
void* conv = charset_conversion_open("utf-8", "iso-8859-1");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, "caf\xc3\xa9", NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "caf\xe9"), 0);
free(out);
charset_conversion_close(conv);
}
static void test_iconv_latin1_to_utf8() {
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, "caf\xe9", NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "caf\xc3\xa9"), 0);
free(out);
charset_conversion_close(conv);
}
static void test_iconv_invalid_sequence_fails() {
int err = 0;
/* 0xff is not a valid UTF-8 sequence. */
void* conv = charset_conversion_open("utf-8", "ascii");
EXPECT_NOT_NULL(conv);
EXPECT_TRUE(charset_convert(conv, "bad\xff", &err) == NULL);
EXPECT_TRUE(err == EILSEQ || err == EINVAL);
charset_conversion_close(conv);
}
static void test_iconv_unrepresentable_fails() {
/* "caf\xc3\xa9" (UTF-8 for cafe) has no ASCII representation. */
void* conv = charset_conversion_open("utf-8", "ascii");
EXPECT_NOT_NULL(conv);
EXPECT_TRUE(charset_convert(conv, "caf\xc3\xa9", NULL) == NULL);
charset_conversion_close(conv);
}
/* A latin1 high-bit byte expands to two UTF-8 bytes. With exactly 16 high
* bytes the output is exactly cap = in_len + 16, so the final iconv call fills
* the buffer completely and a naive NUL-terminator write would overflow. */
static void test_iconv_exact_fill_no_overflow() {
char name[64];
strcpy(name, "dir/");
int n = 4;
for (int i = 0; i < 16; i++)
name[n++] = (char)(0x80 + i);
name[n] = '\0';
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, name, NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT((int)strlen(out), n + 16);
charset_conversion_close(conv);
free(out);
}
/* Many high-bit bytes force the output buffer past its initial cap, exercising
* the E2BIG growth path (input partially consumed/produced before the grow). */
static void test_iconv_growth_expanding_name() {
char name[256];
strcpy(name, "dir/");
int n = 4;
for (int i = 0; i < 80; i++)
name[n++] = (char)(0x80 + (i % 0x80));
name[n] = '\0';
void* conv = charset_conversion_open("iso-8859-1", "utf-8");
EXPECT_NOT_NULL(conv);
char* out = charset_convert(conv, name, NULL);
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT((int)strlen(out), n + 80);
charset_conversion_close(conv);
free(out);
}
/* --- process-wide wire conversion ---------------------------------------- */
static void test_iconv_wire_sender_converts_local_to_remote() {
EXPECT_TRUE(charset_wire_init_sender("utf-8,iso-8859-1"));
char* wire = charset_wire_apply("caf\xc3\xa9");
EXPECT_NOT_NULL(wire);
EXPECT_EQ_INT(strcmp(wire, "caf\xe9"), 0);
free(wire);
charset_wire_free();
}
static void test_iconv_wire_receiver_converts_remote_to_local() {
EXPECT_TRUE(charset_wire_init_receiver("utf-8,iso-8859-1", NULL));
char* local = charset_wire_apply("caf\xe9");
EXPECT_NOT_NULL(local);
EXPECT_EQ_INT(strcmp(local, "caf\xc3\xa9"), 0);
free(local);
charset_wire_free();
}
static void test_iconv_wire_disabled_passthrough() {
charset_wire_init_sender(NULL);
EXPECT_FALSE(charset_wire_active());
char* out = charset_wire_apply("plain/name\xff");
EXPECT_NOT_NULL(out);
EXPECT_EQ_INT(strcmp(out, "plain/name\xff"), 0);
free(out);
charset_wire_free();
}
static void test_iconv_wire_str_roundtrip() {
EXPECT_TRUE(charset_wire_init_sender("utf-8,iso-8859-1"));
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
io_set_fds(p[0], p[0]);
charset_wire_free();
charset_wire_init_receiver("utf-8,iso-8859-1", NULL);
char* got = receive_wire_str(p[0]);
bool ok = got != NULL && strcmp(got, "caf\xc3\xa9") == 0;
free(got);
charset_wire_free();
close(p[0]);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
io_set_fds(p[1], p[1]);
bool sent = send_wire_str(p[1], "caf\xc3\xa9");
int status;
waitpid(pid, &status, 0);
close(p[1]);
charset_wire_free();
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
void test_iconv() {
test_iconv_spec_parse_split();
test_iconv_spec_parse_single_defaults_to_local();
test_iconv_spec_parse_garbage();
test_iconv_spec_valid();
test_iconv_utf8_to_latin1();
test_iconv_latin1_to_utf8();
test_iconv_invalid_sequence_fails();
test_iconv_unrepresentable_fails();
test_iconv_exact_fill_no_overflow();
test_iconv_growth_expanding_name();
test_iconv_wire_sender_converts_local_to_remote();
test_iconv_wire_receiver_converts_remote_to_local();
test_iconv_wire_disabled_passthrough();
test_iconv_wire_str_roundtrip();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_ICONV_H
#define TEST_ICONV_H
void test_iconv(void);
#endif
+150
View File
@@ -0,0 +1,150 @@
#include "test_stop.h"
#include "stop_condition.h"
#include "test_utils.h"
#include <limits.h>
#include <time.h>
static void test_stop_after_parse_valid() {
int minutes = 0;
EXPECT_TRUE(stop_parse_after_minutes("5", &minutes));
EXPECT_EQ_INT(minutes, 5);
EXPECT_TRUE(stop_parse_after_minutes("1", &minutes));
EXPECT_EQ_INT(minutes, 1);
EXPECT_TRUE(stop_parse_after_minutes("1440", &minutes));
EXPECT_EQ_INT(minutes, 1440);
EXPECT_TRUE(stop_parse_after_minutes("2147483647", &minutes));
EXPECT_EQ_INT(minutes, INT_MAX);
}
static void test_stop_after_parse_invalid() {
int minutes = 0;
EXPECT_FALSE(stop_parse_after_minutes("0", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("-1", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("abc", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("5x", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("1.5", &minutes));
EXPECT_FALSE(stop_parse_after_minutes(" 5", &minutes));
EXPECT_FALSE(stop_parse_after_minutes(" 5 ", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("+5", &minutes));
EXPECT_FALSE(stop_parse_after_minutes("2147483648", &minutes));
EXPECT_FALSE(stop_parse_after_minutes(NULL, &minutes));
}
static void test_stop_at_parse_hhmm() {
time_t now = 1700000000;
time_t deadline = 0;
EXPECT_TRUE(stop_parse_at_time("12:30", now, &deadline));
struct tm t;
EXPECT_NOT_NULL(localtime_r(&deadline, &t));
EXPECT_EQ_INT(t.tm_hour, 12);
EXPECT_EQ_INT(t.tm_min, 30);
EXPECT_EQ_INT(t.tm_sec, 0);
EXPECT_TRUE(stop_parse_at_time("12:30:59", now, &deadline));
EXPECT_NOT_NULL(localtime_r(&deadline, &t));
EXPECT_EQ_INT(t.tm_hour, 12);
EXPECT_EQ_INT(t.tm_min, 30);
EXPECT_EQ_INT(t.tm_sec, 59);
EXPECT_TRUE(stop_parse_at_time("00:00", now, &deadline));
EXPECT_NOT_NULL(localtime_r(&deadline, &t));
EXPECT_EQ_INT(t.tm_hour, 0);
EXPECT_EQ_INT(t.tm_min, 0);
EXPECT_EQ_INT(t.tm_sec, 0);
}
static void test_stop_at_parse_now_plus() {
time_t now = 1700000000;
time_t deadline = 0;
EXPECT_TRUE(stop_parse_at_time("now+90s", now, &deadline));
EXPECT_EQ_INT(deadline, now + 90);
EXPECT_TRUE(stop_parse_at_time("now+5m", now, &deadline));
EXPECT_EQ_INT(deadline, now + 300);
EXPECT_TRUE(stop_parse_at_time("now+2h", now, &deadline));
EXPECT_EQ_INT(deadline, now + 7200);
EXPECT_TRUE(stop_parse_at_time("now+1d", now, &deadline));
EXPECT_EQ_INT(deadline, now + 86400);
EXPECT_TRUE(stop_parse_at_time("now+0s", now, &deadline));
EXPECT_EQ_INT(deadline, now);
}
static void test_stop_at_parse_invalid() {
time_t now = 1700000000;
time_t deadline = 0;
EXPECT_FALSE(stop_parse_at_time("12", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12:3", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("1234", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12:30:5", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12:30:5x", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("24:00", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12:60", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12:30:61", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("12;00", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now+", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now+5", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now+5x", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now-5m", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now+1w", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now+ 5s", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("now++5s", now, &deadline));
/* Signed overflow of the destination deadline must be rejected, not wrap. */
EXPECT_FALSE(stop_parse_at_time("now+9223372036854775807s", now, &deadline));
/* 10^15 days is well beyond LONG_MAX/86400, so the amount itself is rejected. */
EXPECT_FALSE(stop_parse_at_time("now+1000000000000000d", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("abc", now, &deadline));
EXPECT_FALSE(stop_parse_at_time("", now, &deadline));
EXPECT_FALSE(stop_parse_at_time(NULL, now, &deadline));
}
static void test_stop_deadline_latency() {
struct timespec now;
EXPECT_EQ_INT(clock_gettime(CLOCK_MONOTONIC, &now), 0);
StopCondition future = stop_condition_make(true, 60, false, 0, now);
EXPECT_TRUE(future.has_monotonic);
EXPECT_EQ_INT(future.monotonic_deadline.tv_sec, now.tv_sec + 3600);
EXPECT_EQ_INT(future.monotonic_deadline.tv_nsec, now.tv_nsec);
EXPECT_FALSE(future.has_wall);
EXPECT_FALSE(stop_condition_reached(&future));
/* Move the 60-minute deadline into the past: the check now reports reached. */
StopCondition past = stop_condition_make(true, 60, false, 0, now);
past.monotonic_deadline.tv_sec -= 7200;
EXPECT_TRUE(stop_condition_reached(&past));
StopCondition no_after = stop_condition_make(false, 0, false, 0, now);
EXPECT_FALSE(no_after.has_monotonic);
EXPECT_FALSE(no_after.has_wall);
EXPECT_FALSE(stop_condition_reached(&no_after));
/* An invalid (non-positive) after_minutes never arms the monotonic half. */
StopCondition zero_after = stop_condition_make(true, 0, false, 0, now);
EXPECT_FALSE(zero_after.has_monotonic);
StopCondition neg_after = stop_condition_make(true, -5, false, 0, now);
EXPECT_FALSE(neg_after.has_monotonic);
/* --stop-at: a wall-clock deadline in the past/now is reached; one in the
future is not, and it stays independent of the monotonic half. */
StopCondition wall_future = stop_condition_make(false, 0, true, time(NULL) + 3600, now);
EXPECT_TRUE(wall_future.has_wall);
EXPECT_FALSE(wall_future.has_monotonic);
EXPECT_FALSE(stop_condition_reached(&wall_future));
StopCondition wall_past = stop_condition_make(false, 0, true, time(NULL) - 1, now);
EXPECT_TRUE(stop_condition_reached(&wall_past));
EXPECT_FALSE(stop_condition_reached(NULL));
}
void test_stop(void) {
test_stop_after_parse_valid();
test_stop_after_parse_invalid();
test_stop_at_parse_hhmm();
test_stop_at_parse_now_plus();
test_stop_at_parse_invalid();
test_stop_deadline_latency();
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef TEST_STOP_H
#define TEST_STOP_H
void test_stop(void);
#endif