Compare commits
5 Commits
98cb8e212e
...
9985a8f6a4
| Author | SHA1 | Date | |
|---|---|---|---|
| 9985a8f6a4 | |||
| 4534284fe9 | |||
| 2ce4f9fbf6 | |||
| 2450b90dd0 | |||
| e8e9436879 |
@@ -2,7 +2,7 @@ name: CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -73,6 +73,13 @@ jobs:
|
|||||||
- name: Build fuzz targets
|
- name: Build fuzz targets
|
||||||
run: cmake --build build-fuzz -j$(nproc)
|
run: cmake --build build-fuzz -j$(nproc)
|
||||||
|
|
||||||
|
- name: Smoke fuzz targets
|
||||||
|
run: |
|
||||||
|
for target in build-fuzz/fuzz_*; do
|
||||||
|
[ -x "$target" ] || continue
|
||||||
|
timeout 10s "$target" -runs=100 -max_total_time=5
|
||||||
|
done
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e
|
|||||||
| `STATUS_NEXT` | Ready for next file (per-file mode) |
|
| `STATUS_NEXT` | Ready for next file (per-file mode) |
|
||||||
| `STATUS_CHUNK` | Following data is a serialized chunk |
|
| `STATUS_CHUNK` | Following data is a serialized chunk |
|
||||||
| `STATUS_MANIFEST` | Following data is a file manifest (for `--delete`) |
|
| `STATUS_MANIFEST` | Following data is a file manifest (for `--delete`) |
|
||||||
| `STATUS_CHECK` | Incremental check: client sends file path + size + mtime, server responds with OK (skip) or NEXT (send) |
|
| `STATUS_CHECK` | Incremental check: client sends file path + size + mtime and, when negotiated, checksum; server responds with OK (skip) or NEXT (send) |
|
||||||
| `STATUS_CHECK_BATCH` | Batch incremental check: multiple file checks sent in one message |
|
| `STATUS_CHECK_BATCH` | Batch incremental check: multiple file checks sent in one message |
|
||||||
| `STATUS_KEEPALIVE` | Keep-alive heartbeat to detect stalled connections |
|
| `STATUS_KEEPALIVE` | Keep-alive heartbeat to detect stalled connections |
|
||||||
| `STATUS_ABORT` | Abort signal: client interrupts, server cleans up and exits |
|
| `STATUS_ABORT` | Abort signal: client interrupts, server cleans up and exits |
|
||||||
@@ -95,7 +95,9 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up
|
|||||||
|
|
||||||
### Protocol Version
|
### Protocol Version
|
||||||
|
|
||||||
`1.3.0` — server and client must match. Mismatch results in `STATUS_ERROR`.
|
`2.2.0` — server and client must match. This version adds a 64-bit XXH64 checksum to checksum-enabled `STATUS_CHECK` messages and validates the negotiated compression choice (`zstd` or `none`). Older clients and servers must not be mixed with this version; mismatch results in `STATUS_ERROR`.
|
||||||
|
|
||||||
|
Config negotiation is sender-driven: the client serializes transfer options and the server applies them while receiving and writing files. `--checksum` compares size and content checksum instead of timestamps. `--compress-choice zstd` enables zstd; `none` disables it. Unsupported choices are rejected during config exchange.
|
||||||
|
|
||||||
## Command-Line Arguments
|
## Command-Line Arguments
|
||||||
|
|
||||||
@@ -186,7 +188,7 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up
|
|||||||
2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed
|
2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed
|
||||||
3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream`
|
3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream`
|
||||||
4. **Network protocol** — status-code-driven exchange with metadata packing, keep-alive, and abort support
|
4. **Network protocol** — status-code-driven exchange with metadata packing, keep-alive, and abort support
|
||||||
5. **Incremental check** — client sends `STATUS_CHECK` + path + size + mtime; server compares against destination. Can be batched via `STATUS_CHECK_BATCH` for reduced round-trips.
|
5. **Incremental check** — client sends `STATUS_CHECK` + path + size + mtime and, with `--checksum`, XXH64 content checksum; server compares against destination. Can be batched via `STATUS_CHECK_BATCH` for reduced round-trips.
|
||||||
6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks
|
6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks
|
||||||
7. **Metadata restoration** — `chmod()`, `chown()`, `utimensat()` on the receiving side
|
7. **Metadata restoration** — `chmod()`, `chown()`, `utimensat()` on the receiving side
|
||||||
8. **`--delete`** — sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories
|
8. **`--delete`** — sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories
|
||||||
|
|||||||
@@ -461,6 +461,14 @@ static int parse_args(Config* config, int argc, char* argv[], int* positional_ar
|
|||||||
return -1;
|
return -1;
|
||||||
free(config->compress_choice);
|
free(config->compress_choice);
|
||||||
config->compress_choice = dup;
|
config->compress_choice = dup;
|
||||||
|
if (strcmp(config->compress_choice, "zstd") == 0)
|
||||||
|
config->use_compression = true;
|
||||||
|
else if (strcmp(config->compress_choice, "none") == 0)
|
||||||
|
config->use_compression = false;
|
||||||
|
else {
|
||||||
|
fprintf(stderr, "Error: --compress-choice must be 'zstd' or 'none'\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
} else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) {
|
||||||
int val;
|
int val;
|
||||||
if (!parse_positive_int(argv[++i], &val)) {
|
if (!parse_positive_int(argv[++i], &val)) {
|
||||||
@@ -517,6 +525,12 @@ static bool validate_config(const Config* config) {
|
|||||||
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
|
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (config->append || config->append_verify) {
|
||||||
|
fprintf(
|
||||||
|
stderr,
|
||||||
|
"Error: --append and --append-verify are not supported yet; refusing to ignore option\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (config->use_tls) {
|
if (config->use_tls) {
|
||||||
if (!config->tls_cert || !config->tls_key) {
|
if (!config->tls_cert || !config->tls_key) {
|
||||||
fprintf(stderr, "Error: --tls requires --cert and --key\n");
|
fprintf(stderr, "Error: --tls requires --cert and --key\n");
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ static int send_delete_manifest(int fd, ArrayList* manifest) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) {
|
static int incremental_check(Client* client, File* file, const Config* config,
|
||||||
|
DeltaSignature** out_sig) {
|
||||||
*out_sig = NULL;
|
*out_sig = NULL;
|
||||||
if (!send_status(client->file_descriptor, STATUS_CHECK))
|
if (!send_status(client->file_descriptor, STATUS_CHECK))
|
||||||
return -1;
|
return -1;
|
||||||
@@ -79,6 +80,12 @@ static int incremental_check(Client* client, File* file, DeltaSignature** out_si
|
|||||||
return -1;
|
return -1;
|
||||||
if (!send_n_data(client->file_descriptor, &mtime, sizeof(mtime)))
|
if (!send_n_data(client->file_descriptor, &mtime, sizeof(mtime)))
|
||||||
return -1;
|
return -1;
|
||||||
|
if (config->checksum) {
|
||||||
|
uint64_t checksum;
|
||||||
|
if (!file_checksum(file, &checksum) ||
|
||||||
|
!send_n_data(client->file_descriptor, &checksum, sizeof(checksum)))
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
Status s;
|
Status s;
|
||||||
if (!receive_status(client->file_descriptor, &s))
|
if (!receive_status(client->file_descriptor, &s))
|
||||||
return -1;
|
return -1;
|
||||||
@@ -176,7 +183,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use
|
|||||||
// Incremental path: use sendfile for the actual data if enabled and no compression
|
// Incremental path: use sendfile for the actual data if enabled and no compression
|
||||||
if (use_sendfile) {
|
if (use_sendfile) {
|
||||||
DeltaSignature* sig = NULL;
|
DeltaSignature* sig = NULL;
|
||||||
int rc = incremental_check(client, file, &sig);
|
int rc = incremental_check(client, file, config, &sig);
|
||||||
if (rc == 1) {
|
if (rc == 1) {
|
||||||
delta_signature_destroy(sig);
|
delta_signature_destroy(sig);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -202,7 +209,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use
|
|||||||
// Incremental path with single_calls (supports compression and delta)
|
// Incremental path with single_calls (supports compression and delta)
|
||||||
file_send_fn send_fn = (file_send_fn)file_send_single_calls;
|
file_send_fn send_fn = (file_send_fn)file_send_single_calls;
|
||||||
DeltaSignature* sig = NULL;
|
DeltaSignature* sig = NULL;
|
||||||
int rc = incremental_check(client, file, &sig);
|
int rc = incremental_check(client, file, config, &sig);
|
||||||
if (rc < 0) {
|
if (rc < 0) {
|
||||||
delta_signature_destroy(sig);
|
delta_signature_destroy(sig);
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
+76
-8
@@ -15,6 +15,25 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static char* authorized_root;
|
||||||
|
static bool allow_delete;
|
||||||
|
|
||||||
|
static bool path_is_within(const char* root, const char* path) {
|
||||||
|
size_t n = strlen(root);
|
||||||
|
return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool __attribute__((unused)) configure_authorization(const char* root) {
|
||||||
|
char resolved[PATH_MAX];
|
||||||
|
if (!root || !realpath(root, resolved))
|
||||||
|
return false;
|
||||||
|
authorized_root = str_dup(resolved);
|
||||||
|
return authorized_root != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
int receive_files(Config* config, int fd) {
|
int receive_files(Config* config, int fd) {
|
||||||
Status status;
|
Status status;
|
||||||
@@ -39,7 +58,7 @@ int receive_files(Config* config, int fd) {
|
|||||||
if (file == NULL && !skipped)
|
if (file == NULL && !skipped)
|
||||||
return -1;
|
return -1;
|
||||||
if (config->save_to_disk)
|
if (config->save_to_disk)
|
||||||
file_save_to_disk(config->receive_root_directory, file, NULL);
|
file_save_to_disk(config->receive_root_directory, file, config);
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
} else if (status == STATUS_CHUNK) {
|
} else if (status == STATUS_CHUNK) {
|
||||||
Chunk* chunk = receive_chunk_data(fd, config);
|
Chunk* chunk = receive_chunk_data(fd, config);
|
||||||
@@ -49,7 +68,7 @@ int receive_files(Config* config, int fd) {
|
|||||||
}
|
}
|
||||||
for (int i = 0; i < chunk->element_count; i++) {
|
for (int i = 0; i < chunk->element_count; i++) {
|
||||||
if (config->save_to_disk)
|
if (config->save_to_disk)
|
||||||
file_save_to_disk(config->receive_root_directory, chunk->items[i], NULL);
|
file_save_to_disk(config->receive_root_directory, chunk->items[i], config);
|
||||||
}
|
}
|
||||||
chunk_destroy(chunk);
|
chunk_destroy(chunk);
|
||||||
} else if (status == STATUS_CHECK_BATCH) {
|
} else if (status == STATUS_CHECK_BATCH) {
|
||||||
@@ -88,7 +107,7 @@ int receive_files(Config* config, int fd) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (config->save_to_disk)
|
if (config->save_to_disk)
|
||||||
file_save_to_disk(config->receive_root_directory, file, NULL);
|
file_save_to_disk(config->receive_root_directory, file, config);
|
||||||
file_destroy(file);
|
file_destroy(file);
|
||||||
}
|
}
|
||||||
next:
|
next:
|
||||||
@@ -119,6 +138,36 @@ void handler(int file_descriptor) {
|
|||||||
close(file_descriptor);
|
close(file_descriptor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!authorized_root) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
|
config_delete(config);
|
||||||
|
close(file_descriptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char resolved_destination[PATH_MAX];
|
||||||
|
char* canonical_destination = realpath(config->receive_root_directory, NULL);
|
||||||
|
const char* destination =
|
||||||
|
canonical_destination ? canonical_destination : config->receive_root_directory;
|
||||||
|
if (has_path_traversal(destination) || !path_is_within(authorized_root, destination)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
|
||||||
|
free(canonical_destination);
|
||||||
|
config_delete(config);
|
||||||
|
close(file_descriptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canonical_destination)
|
||||||
|
snprintf(resolved_destination, sizeof(resolved_destination), "%s", canonical_destination);
|
||||||
|
else
|
||||||
|
snprintf(resolved_destination, sizeof(resolved_destination), "%s", destination);
|
||||||
|
free(canonical_destination);
|
||||||
|
free(config->receive_root_directory);
|
||||||
|
config->receive_root_directory = str_dup(resolved_destination);
|
||||||
|
if (!config->receive_root_directory) {
|
||||||
|
config_delete(config);
|
||||||
|
close(file_descriptor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config->use_delete = config->use_delete && allow_delete;
|
||||||
if (config->use_multithreading) {
|
if (config->use_multithreading) {
|
||||||
Queue* q = queue_create(100, file_destroy);
|
Queue* q = queue_create(100, file_destroy);
|
||||||
if (q == NULL) {
|
if (q == NULL) {
|
||||||
@@ -142,8 +191,11 @@ void handler(int file_descriptor) {
|
|||||||
close(file_descriptor);
|
close(file_descriptor);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thrd_join(receiver, NULL);
|
int receiver_result;
|
||||||
thrd_join(writer, NULL);
|
int writer_result;
|
||||||
|
thrd_join(receiver, &receiver_result);
|
||||||
|
thrd_join(writer, &writer_result);
|
||||||
|
if (receiver_result == thrd_success && writer_result == thrd_success)
|
||||||
send_status(file_descriptor, STATUS_OK);
|
send_status(file_descriptor, STATUS_OK);
|
||||||
pipeline_context_receiver_destroy(context);
|
pipeline_context_receiver_destroy(context);
|
||||||
} else {
|
} else {
|
||||||
@@ -175,6 +227,8 @@ static void print_server_usage(void) {
|
|||||||
printf(" --cert <path> TLS certificate file (PEM)\n");
|
printf(" --cert <path> TLS certificate file (PEM)\n");
|
||||||
printf(" --key <path> TLS private key file (PEM)\n");
|
printf(" --key <path> TLS private key file (PEM)\n");
|
||||||
printf(" --ca <path> TLS CA certificate file (PEM)\n");
|
printf(" --ca <path> TLS CA certificate file (PEM)\n");
|
||||||
|
printf(" --destination-root <path> Authorized destination root (default: .)\n");
|
||||||
|
printf(" --allow-delete Permit manifest deletion\n");
|
||||||
printf(" -v, --verbose Enable debug logging\n");
|
printf(" -v, --verbose Enable debug logging\n");
|
||||||
printf(" --help Show this help\n");
|
printf(" --help Show this help\n");
|
||||||
}
|
}
|
||||||
@@ -185,6 +239,8 @@ int main(int argc, char* argv[]) {
|
|||||||
char* tls_key = NULL;
|
char* tls_key = NULL;
|
||||||
char* tls_ca = NULL;
|
char* tls_ca = NULL;
|
||||||
int port = 8080;
|
int port = 8080;
|
||||||
|
const char* destination_root = ".";
|
||||||
|
bool stdio_mode = false;
|
||||||
|
|
||||||
signal(SIGPIPE, SIG_IGN);
|
signal(SIGPIPE, SIG_IGN);
|
||||||
for (int i = 1; i < argc; i++) {
|
for (int i = 1; i < argc; i++) {
|
||||||
@@ -192,9 +248,7 @@ int main(int argc, char* argv[]) {
|
|||||||
print_server_usage();
|
print_server_usage();
|
||||||
return 0;
|
return 0;
|
||||||
} else if (strcmp(argv[i], "--stdio") == 0) {
|
} else if (strcmp(argv[i], "--stdio") == 0) {
|
||||||
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
|
stdio_mode = true;
|
||||||
handler(STDIN_FILENO);
|
|
||||||
return 0;
|
|
||||||
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||||
set_log_level(LOG_LEVEL_DEBUG);
|
set_log_level(LOG_LEVEL_DEBUG);
|
||||||
} else if (strcmp(argv[i], "--tls") == 0) {
|
} else if (strcmp(argv[i], "--tls") == 0) {
|
||||||
@@ -205,6 +259,10 @@ int main(int argc, char* argv[]) {
|
|||||||
tls_key = argv[++i];
|
tls_key = argv[++i];
|
||||||
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
|
||||||
tls_ca = argv[++i];
|
tls_ca = argv[++i];
|
||||||
|
} else if (strcmp(argv[i], "--destination-root") == 0 && i + 1 < argc) {
|
||||||
|
destination_root = argv[++i];
|
||||||
|
} else if (strcmp(argv[i], "--allow-delete") == 0) {
|
||||||
|
allow_delete = true;
|
||||||
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||||
char* end;
|
char* end;
|
||||||
long p = strtol(argv[++i], &end, 10);
|
long p = strtol(argv[++i], &end, 10);
|
||||||
@@ -226,6 +284,16 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
signal(SIGINT, cleanup);
|
signal(SIGINT, cleanup);
|
||||||
signal(SIGTERM, cleanup);
|
signal(SIGTERM, cleanup);
|
||||||
|
if (!configure_authorization(destination_root)) {
|
||||||
|
fprintf(stderr, "Error: invalid destination root '%s'\n", destination_root);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (stdio_mode) {
|
||||||
|
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
|
||||||
|
handler(STDIN_FILENO);
|
||||||
|
free(authorized_root);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
g_server = server_create(port);
|
g_server = server_create(port);
|
||||||
if (g_server == NULL) {
|
if (g_server == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to create server");
|
log_message(LOG_LEVEL_ERROR, "Failed to create server");
|
||||||
|
|||||||
@@ -469,6 +469,12 @@ Config* config_receive(int file_descriptor) {
|
|||||||
config->compress_choice = receive_str(file_descriptor);
|
config->compress_choice = receive_str(file_descriptor);
|
||||||
if (config->compress_choice == NULL)
|
if (config->compress_choice == NULL)
|
||||||
goto error;
|
goto error;
|
||||||
|
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||||
|
strcmp(config->compress_choice, "none") != 0) {
|
||||||
|
fprintf(stderr, "Unsupported compression choice: %s\n", config->compress_choice);
|
||||||
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
|
goto error;
|
||||||
|
}
|
||||||
config->address = NULL;
|
config->address = NULL;
|
||||||
config->bind_address = NULL;
|
config->bind_address = NULL;
|
||||||
config->ipv6 = false;
|
config->ipv6 = false;
|
||||||
|
|||||||
+1
-1
@@ -128,7 +128,7 @@ typedef struct Config {
|
|||||||
char* compress_choice;
|
char* compress_choice;
|
||||||
} Config;
|
} Config;
|
||||||
|
|
||||||
#define PROTOCOL_VERSION "1.3.0"
|
#define PROTOCOL_VERSION "2.2.0"
|
||||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||||
|
|
||||||
Config* config_create(void);
|
Config* config_create(void);
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ uint32_t delta_xxhash32(const void* data, uint32_t len) {
|
|||||||
return XXH32(data, len, 0);
|
return XXH32(data, len, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint64_t delta_xxhash64(const void* data, size_t len) {
|
||||||
|
return XXH64(data, len, 0);
|
||||||
|
}
|
||||||
|
|
||||||
DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_file_size,
|
DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_file_size,
|
||||||
uint32_t block_size) {
|
uint32_t block_size) {
|
||||||
if (old_file_data == NULL || old_file_size == 0 || block_size == 0)
|
if (old_file_data == NULL || old_file_size == 0 || block_size == 0)
|
||||||
|
|||||||
@@ -72,5 +72,6 @@ bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size);
|
|||||||
|
|
||||||
uint32_t delta_adler32(const void* data, uint32_t len);
|
uint32_t delta_adler32(const void* data, uint32_t len);
|
||||||
uint32_t delta_xxhash32(const void* data, uint32_t len);
|
uint32_t delta_xxhash32(const void* data, uint32_t len);
|
||||||
|
uint64_t delta_xxhash64(const void* data, size_t len);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+157
-5
@@ -2,6 +2,7 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <libgen.h>
|
#include <libgen.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -21,6 +22,19 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
|
bool file_checksum(File* file, uint64_t* checksum) {
|
||||||
|
if (!file || !checksum || !file->data)
|
||||||
|
return false;
|
||||||
|
if (file->data->size == 0) {
|
||||||
|
*checksum = delta_xxhash64("", 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!file->data->data && !file_load_data(file))
|
||||||
|
return false;
|
||||||
|
*checksum = delta_xxhash64(file->data->data, file->data->size);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
File* file_create(const char* path) {
|
File* file_create(const char* path) {
|
||||||
File* file = (File*)malloc(sizeof(File));
|
File* file = (File*)malloc(sizeof(File));
|
||||||
if (file == NULL) {
|
if (file == NULL) {
|
||||||
@@ -162,6 +176,17 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --update is receiver-side policy: never replace a newer destination. */
|
||||||
|
if (config && config->update) {
|
||||||
|
struct stat destination_stat;
|
||||||
|
if (stat(disk_path, &destination_stat) == 0 && file->metadata &&
|
||||||
|
destination_stat.st_mtime > file->metadata->mtime_sec) {
|
||||||
|
free(resolved_root);
|
||||||
|
free(disk_path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (backup_enabled) {
|
if (backup_enabled) {
|
||||||
struct stat backup_stat;
|
struct stat backup_stat;
|
||||||
if (stat(disk_path, &backup_stat) == 0) {
|
if (stat(disk_path, &backup_stat) == 0) {
|
||||||
@@ -421,12 +446,18 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
|
|
||||||
unsigned long long check_size;
|
unsigned long long check_size;
|
||||||
long long check_mtime;
|
long long check_mtime;
|
||||||
|
uint64_t check_checksum = 0;
|
||||||
if (!receive_n_data(fd, &check_size, sizeof(check_size)) ||
|
if (!receive_n_data(fd, &check_size, sizeof(check_size)) ||
|
||||||
!receive_n_data(fd, &check_mtime, sizeof(check_mtime))) {
|
!receive_n_data(fd, &check_mtime, sizeof(check_mtime))) {
|
||||||
free(check_path);
|
free(check_path);
|
||||||
send_status(fd, STATUS_ERROR);
|
send_status(fd, STATUS_ERROR);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
if (config->checksum && !receive_n_data(fd, &check_checksum, sizeof(check_checksum))) {
|
||||||
|
free(check_path);
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (has_path_traversal(check_path)) {
|
if (has_path_traversal(check_path)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path);
|
log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path);
|
||||||
@@ -440,8 +471,17 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
bool has_old_file = (full_path && lstat(full_path, &st) == 0);
|
bool has_old_file = (full_path && lstat(full_path, &st) == 0);
|
||||||
unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0;
|
unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0;
|
||||||
|
|
||||||
bool match = has_old_file && (unsigned long long)st.st_size == check_size &&
|
bool match = has_old_file && (unsigned long long)st.st_size == check_size;
|
||||||
(long long)st.st_mtime == check_mtime;
|
if (match && config->checksum) {
|
||||||
|
void* old_data = old_size > 0 ? old_data_from_path(full_path, old_size) : NULL;
|
||||||
|
uint64_t old_checksum = old_size == 0 ? delta_xxhash64("", 0) : 0;
|
||||||
|
if (old_data)
|
||||||
|
old_checksum = delta_xxhash64(old_data, (size_t)old_size);
|
||||||
|
match = (old_size == 0 || old_data) && old_checksum == check_checksum;
|
||||||
|
free(old_data);
|
||||||
|
} else if (match) {
|
||||||
|
match = (long long)st.st_mtime == check_mtime;
|
||||||
|
}
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
if (!send_status(fd, STATUS_OK)) {
|
if (!send_status(fd, STATUS_OK)) {
|
||||||
@@ -518,8 +558,108 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
|||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int open_secure_parent(const char* path, char** leaf_out) {
|
||||||
|
char* copy = str_dup(path);
|
||||||
|
if (!copy)
|
||||||
|
return -1;
|
||||||
|
char* parent = dirname(copy);
|
||||||
|
const char* slash = strrchr(path, '/');
|
||||||
|
char* leaf = str_dup(slash ? slash + 1 : path);
|
||||||
|
if (!leaf) {
|
||||||
|
free(copy);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int fd = (parent[0] == '/') ? open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC)
|
||||||
|
: open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||||
|
if (fd < 0) {
|
||||||
|
free(copy);
|
||||||
|
free(leaf);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char* save = NULL;
|
||||||
|
char* component = strtok_r(parent, "/", &save);
|
||||||
|
while (component) {
|
||||||
|
if (strcmp(component, ".") != 0 && strcmp(component, "..") != 0) {
|
||||||
|
int next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
if (next < 0 && errno == ENOENT && mkdirat(fd, component, 0755) == 0)
|
||||||
|
next = openat(fd, component, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
if (next < 0) {
|
||||||
|
close(fd);
|
||||||
|
free(copy);
|
||||||
|
free(leaf);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
close(fd);
|
||||||
|
fd = next;
|
||||||
|
}
|
||||||
|
component = strtok_r(NULL, "/", &save);
|
||||||
|
}
|
||||||
|
free(copy);
|
||||||
|
*leaf_out = leaf;
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool write_all(int fd, const void* data, unsigned long long size) {
|
||||||
|
const unsigned char* p = data;
|
||||||
|
unsigned long long done = 0;
|
||||||
|
while (done < size) {
|
||||||
|
ssize_t n = write(fd, p + done, (size_t)(size - done));
|
||||||
|
if (n < 0 && errno == EINTR)
|
||||||
|
continue;
|
||||||
|
if (n <= 0)
|
||||||
|
return false;
|
||||||
|
done += (unsigned long long)n;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool to_disk_secure(const char* path, const void* data, unsigned long long data_size,
|
||||||
|
bool inplace, bool sparse) {
|
||||||
|
char* leaf = NULL;
|
||||||
|
int dirfd = open_secure_parent(path, &leaf);
|
||||||
|
if (dirfd < 0)
|
||||||
|
return false;
|
||||||
|
int fd = -1;
|
||||||
|
bool ok = false;
|
||||||
|
if (inplace) {
|
||||||
|
fd = openat(dirfd, leaf, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644);
|
||||||
|
if (fd >= 0) {
|
||||||
|
if (!sparse || data_size == 0 || ftruncate(fd, (off_t)data_size) == 0)
|
||||||
|
ok = write_all(fd, data, data_size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char tmp[NAME_MAX];
|
||||||
|
for (unsigned int i = 0; i < 100 && !ok; ++i) {
|
||||||
|
snprintf(tmp, sizeof(tmp), ".%s.tmp.%ld.%u", leaf, (long)getpid(), i);
|
||||||
|
fd = openat(dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600);
|
||||||
|
if (fd < 0)
|
||||||
|
continue;
|
||||||
|
if (sparse && data_size > 0)
|
||||||
|
ok = ftruncate(fd, (off_t)data_size) == 0;
|
||||||
|
if (ok || (!sparse || data_size == 0))
|
||||||
|
ok = write_all(fd, data, data_size);
|
||||||
|
if (close(fd) != 0)
|
||||||
|
ok = false;
|
||||||
|
fd = -1;
|
||||||
|
if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0)
|
||||||
|
ok = false;
|
||||||
|
if (!ok)
|
||||||
|
unlinkat(dirfd, tmp, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fd >= 0)
|
||||||
|
close(fd);
|
||||||
|
close(dirfd);
|
||||||
|
free(leaf);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
bool to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace,
|
bool to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace,
|
||||||
bool sparse) {
|
bool sparse) {
|
||||||
|
if (!path || (!data && data_size != 0) || has_path_traversal(path))
|
||||||
|
return false;
|
||||||
|
return to_disk_secure(path, data, data_size, inplace, sparse);
|
||||||
|
/* Kept below only as historical context; all writes use descriptor-relative operations. */
|
||||||
char* tmp_path = NULL;
|
char* tmp_path = NULL;
|
||||||
char* directory = NULL;
|
char* directory = NULL;
|
||||||
|
|
||||||
@@ -664,6 +804,11 @@ File* file_receive(const Config* config, int file_descriptor) {
|
|||||||
char* path = receive_str(file_descriptor);
|
char* path = receive_str(file_descriptor);
|
||||||
if (path == NULL)
|
if (path == NULL)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
if (path[0] == '\0' || has_path_traversal(path)) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Invalid received file path: %s", path);
|
||||||
|
free(path);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
File* file = file_create(path);
|
File* file = file_create(path);
|
||||||
free(path);
|
free(path);
|
||||||
if (file == NULL)
|
if (file == NULL)
|
||||||
@@ -715,13 +860,20 @@ int receive_manifest(int fd, const Config* config, int* next_status) {
|
|||||||
int count;
|
int count;
|
||||||
if (!receive_int(fd, &count))
|
if (!receive_int(fd, &count))
|
||||||
return -1;
|
return -1;
|
||||||
|
if (count < 0 || count > MAX_MANIFEST_ENTRIES)
|
||||||
|
return -1;
|
||||||
ArrayList* manifest = array_list_create(free);
|
ArrayList* manifest = array_list_create(free);
|
||||||
if (manifest) {
|
if (!manifest)
|
||||||
|
return -1;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
char* s = receive_str(fd);
|
char* s = receive_str(fd);
|
||||||
if (s)
|
if (!s || s[0] == '\0' || has_path_traversal(s) || !array_list_add(manifest, s)) {
|
||||||
array_list_add(manifest, s);
|
free(s);
|
||||||
|
array_list_delete(manifest);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (manifest) {
|
||||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||||
delete_extras(config->receive_root_directory, manifest);
|
delete_extras(config->receive_root_directory, manifest);
|
||||||
array_list_delete(manifest);
|
array_list_delete(manifest);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ typedef struct {
|
|||||||
File* file_create(const char* path);
|
File* file_create(const char* path);
|
||||||
void file_destroy(void* item);
|
void file_destroy(void* item);
|
||||||
bool file_load_data(File* file);
|
bool file_load_data(File* file);
|
||||||
|
bool file_checksum(File* file, uint64_t* checksum);
|
||||||
File* file_receive(const Config* config, int file_descriptor);
|
File* file_receive(const Config* config, int file_descriptor);
|
||||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||||
int compression_level, bool send_path);
|
int compression_level, bool send_path);
|
||||||
|
|||||||
+12
-1
@@ -105,11 +105,16 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
|
|||||||
*ok = 0;
|
*ok = 0;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (!present) {
|
if (present == 0) {
|
||||||
if (ok)
|
if (ok)
|
||||||
*ok = 1;
|
*ok = 1;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
if (present != 1) {
|
||||||
|
if (ok)
|
||||||
|
*ok = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||||
if (m == NULL) {
|
if (m == NULL) {
|
||||||
if (ok)
|
if (ok)
|
||||||
@@ -156,6 +161,12 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
m->mtime_nsec = (long)mtime_nsec;
|
m->mtime_nsec = (long)mtime_nsec;
|
||||||
|
if (mtime_nsec < 0 || mtime_nsec >= 1000000000LL || mode < 0 || uid < 0 || gid < 0) {
|
||||||
|
free(m);
|
||||||
|
if (ok)
|
||||||
|
*ok = 0;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
if (ok)
|
if (ok)
|
||||||
*ok = 1;
|
*ok = 1;
|
||||||
return m;
|
return m;
|
||||||
|
|||||||
@@ -101,7 +101,21 @@ static bool receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver*
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void receiver_thread_fail(PipelineContextReceiver* context) {
|
||||||
|
mtx_lock(&context->mutex);
|
||||||
|
context->cancelled = true;
|
||||||
|
context->receiver_done = true;
|
||||||
|
cnd_broadcast(&context->condition_not_empty);
|
||||||
|
cnd_broadcast(&context->condition_not_full);
|
||||||
|
mtx_unlock(&context->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
int receive_thread(void* pipeline_context) {
|
int receive_thread(void* pipeline_context) {
|
||||||
|
#define RECEIVE_THREAD_FAIL() \
|
||||||
|
do { \
|
||||||
|
receiver_thread_fail(context); \
|
||||||
|
return thrd_error; \
|
||||||
|
} while (0)
|
||||||
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
||||||
if (context->ssl)
|
if (context->ssl)
|
||||||
io_set_ssl(context->ssl);
|
io_set_ssl(context->ssl);
|
||||||
@@ -112,53 +126,52 @@ int receive_thread(void* pipeline_context) {
|
|||||||
|
|
||||||
Status status;
|
Status status;
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
||||||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
|
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
|
||||||
if (status == STATUS_KEEPALIVE) {
|
if (status == STATUS_KEEPALIVE) {
|
||||||
send_status(file_descriptor, STATUS_KEEPALIVE);
|
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
||||||
|
RECEIVE_THREAD_FAIL();
|
||||||
goto next;
|
goto next;
|
||||||
}
|
}
|
||||||
if (status == STATUS_ABORT) {
|
if (status == STATUS_ABORT) {
|
||||||
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
}
|
}
|
||||||
if (status == STATUS_CHECK) {
|
if (status == STATUS_CHECK) {
|
||||||
bool skipped;
|
bool skipped;
|
||||||
File* file = receive_incremental_check(file_descriptor, config, &skipped);
|
File* file = receive_incremental_check(file_descriptor, config, &skipped);
|
||||||
if (!skipped) {
|
if (!skipped) {
|
||||||
if (file == NULL)
|
if (file == NULL)
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||||
&context->condition_not_empty, &context->condition_not_full);
|
&context->condition_not_empty, &context->condition_not_full);
|
||||||
}
|
}
|
||||||
} else if (status == STATUS_CHUNK) {
|
} else if (status == STATUS_CHUNK) {
|
||||||
if (!receive_chunk_enqueue(file_descriptor, context))
|
if (!receive_chunk_enqueue(file_descriptor, context))
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
} else if (status == STATUS_CHECK_BATCH) {
|
} else if (status == STATUS_CHECK_BATCH) {
|
||||||
int count;
|
int count;
|
||||||
if (!receive_int(file_descriptor, &count))
|
if (!receive_int(file_descriptor, &count))
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
char* check_path = receive_str(file_descriptor);
|
char* check_path = receive_str(file_descriptor);
|
||||||
if (!check_path)
|
if (!check_path)
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
unsigned long long check_size;
|
unsigned long long check_size;
|
||||||
long long check_mtime;
|
long long check_mtime;
|
||||||
if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) ||
|
if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) ||
|
||||||
!receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime))) {
|
!receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime))) {
|
||||||
free(check_path);
|
free(check_path);
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
}
|
}
|
||||||
char* full_path = path_cat(config->receive_root_directory, check_path);
|
char* full_path = path_cat(config->receive_root_directory, check_path);
|
||||||
struct stat st;
|
struct stat st;
|
||||||
bool has_old = full_path && lstat(full_path, &st) == 0;
|
bool has_old = full_path && lstat(full_path, &st) == 0;
|
||||||
bool match = has_old && (unsigned long long)st.st_size == check_size &&
|
bool match = has_old && (unsigned long long)st.st_size == check_size &&
|
||||||
(long long)st.st_mtime == check_mtime;
|
(long long)st.st_mtime == check_mtime;
|
||||||
if (match)
|
if (!send_status(file_descriptor, match ? STATUS_OK : STATUS_NEXT))
|
||||||
send_status(file_descriptor, STATUS_OK);
|
RECEIVE_THREAD_FAIL();
|
||||||
else
|
|
||||||
send_status(file_descriptor, STATUS_NEXT);
|
|
||||||
free(full_path);
|
free(full_path);
|
||||||
free(check_path);
|
free(check_path);
|
||||||
}
|
}
|
||||||
@@ -170,21 +183,24 @@ int receive_thread(void* pipeline_context) {
|
|||||||
&context->condition_not_empty, &context->condition_not_full);
|
&context->condition_not_empty, &context->condition_not_full);
|
||||||
} else {
|
} else {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next:
|
next:
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
}
|
}
|
||||||
if (status == STATUS_MANIFEST) {
|
if (status == STATUS_MANIFEST) {
|
||||||
if (receive_manifest(file_descriptor, config, &status) != 0)
|
if (receive_manifest(file_descriptor, config, &status) != 0)
|
||||||
return thrd_error;
|
RECEIVE_THREAD_FAIL();
|
||||||
}
|
}
|
||||||
|
if (status != STATUS_FINISHED)
|
||||||
|
RECEIVE_THREAD_FAIL();
|
||||||
mtx_lock(&context->mutex);
|
mtx_lock(&context->mutex);
|
||||||
context->receiver_done = true;
|
context->receiver_done = true;
|
||||||
cnd_signal(&context->condition_not_empty);
|
cnd_signal(&context->condition_not_empty);
|
||||||
mtx_unlock(&context->mutex);
|
mtx_unlock(&context->mutex);
|
||||||
|
#undef RECEIVE_THREAD_FAIL
|
||||||
return thrd_success;
|
return thrd_success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ typedef struct {
|
|||||||
mtx_t mutex_progress;
|
mtx_t mutex_progress;
|
||||||
unsigned long long progress_bytes;
|
unsigned long long progress_bytes;
|
||||||
bool sender_done;
|
bool sender_done;
|
||||||
|
bool cancelled;
|
||||||
} PipelineContextSender;
|
} PipelineContextSender;
|
||||||
|
|
||||||
typedef struct PipelineContextReceiver {
|
typedef struct PipelineContextReceiver {
|
||||||
@@ -37,6 +38,7 @@ typedef struct PipelineContextReceiver {
|
|||||||
cnd_t condition_not_full;
|
cnd_t condition_not_full;
|
||||||
cnd_t condition_not_empty;
|
cnd_t condition_not_empty;
|
||||||
bool receiver_done;
|
bool receiver_done;
|
||||||
|
bool cancelled;
|
||||||
} PipelineContextReceiver;
|
} PipelineContextReceiver;
|
||||||
|
|
||||||
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
|
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
|
||||||
|
|||||||
+22
-4
@@ -1,6 +1,7 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <openssl/ssl.h>
|
#include <openssl/ssl.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -75,6 +76,17 @@ static int io_fd(int dir_fd, int file_descriptor) {
|
|||||||
return (dir_fd != -1) ? dir_fd : file_descriptor;
|
return (dir_fd != -1) ? dir_fd : file_descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int deadline_remaining_ms(const struct timespec* deadline) {
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
long long ns =
|
||||||
|
(long long)(deadline->tv_sec - now.tv_sec) * 1000000000LL + deadline->tv_nsec - now.tv_nsec;
|
||||||
|
if (ns <= 0)
|
||||||
|
return 0;
|
||||||
|
long long ms = (ns + 999999) / 1000000;
|
||||||
|
return ms > INT_MAX ? INT_MAX : (int)ms;
|
||||||
|
}
|
||||||
|
|
||||||
bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
||||||
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size);
|
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size);
|
||||||
int fd = io_fd(io_write_fd, file_descriptor);
|
int fd = io_fd(io_write_fd, file_descriptor);
|
||||||
@@ -114,13 +126,19 @@ bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
|
|||||||
|
|
||||||
size_t total_bytes_received = 0;
|
size_t total_bytes_received = 0;
|
||||||
while (total_bytes_received < data_size) {
|
while (total_bytes_received < data_size) {
|
||||||
struct timespec now;
|
struct pollfd pfd = {.fd = fd, .events = POLLIN};
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
int poll_result = poll(&pfd, 1, deadline_remaining_ms(&deadline));
|
||||||
if (now.tv_sec > deadline.tv_sec ||
|
if (poll_result == 0) {
|
||||||
(now.tv_sec == deadline.tv_sec && now.tv_nsec > deadline.tv_nsec)) {
|
|
||||||
log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC);
|
log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (poll_result < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (pfd.revents & (POLLERR | POLLNVAL))
|
||||||
|
return false;
|
||||||
|
|
||||||
ssize_t bytes_received;
|
ssize_t bytes_received;
|
||||||
if (io_ssl)
|
if (io_ssl)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
|
/* Maximum chunk size (64 MB) — prevents unbounded allocation from the wire */
|
||||||
#define MAX_CHUNK_SIZE (64ULL * 1024 * 1024)
|
#define MAX_CHUNK_SIZE (64ULL * 1024 * 1024)
|
||||||
|
#define MAX_MANIFEST_ENTRIES (1024 * 1024)
|
||||||
|
|
||||||
typedef struct ssl_st SSL;
|
typedef struct ssl_st SSL;
|
||||||
|
|
||||||
|
|||||||
+23
-1
@@ -1,4 +1,5 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -7,6 +8,9 @@
|
|||||||
#include "queue.h"
|
#include "queue.h"
|
||||||
|
|
||||||
Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
|
Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
|
||||||
|
if (capacity <= 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
Queue* queue = (Queue*)malloc(sizeof(Queue));
|
Queue* queue = (Queue*)malloc(sizeof(Queue));
|
||||||
if (queue == NULL) {
|
if (queue == NULL) {
|
||||||
perror("ERROR: Could not allocate memory for queue structure");
|
perror("ERROR: Could not allocate memory for queue structure");
|
||||||
@@ -61,7 +65,9 @@ bool queue_is_full(const Queue* queue) {
|
|||||||
static bool queue_double_capacity(Queue* queue) {
|
static bool queue_double_capacity(Queue* queue) {
|
||||||
if (queue == NULL)
|
if (queue == NULL)
|
||||||
return false;
|
return false;
|
||||||
unsigned int new_capacity = queue->capacity * 2;
|
if (queue->capacity > INT_MAX / 2)
|
||||||
|
return false;
|
||||||
|
int new_capacity = queue->capacity * 2;
|
||||||
if (new_capacity <= 1)
|
if (new_capacity <= 1)
|
||||||
new_capacity = 100;
|
new_capacity = 100;
|
||||||
void** new_items = malloc(new_capacity * sizeof(void*));
|
void** new_items = malloc(new_capacity * sizeof(void*));
|
||||||
@@ -103,6 +109,22 @@ bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t*
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex,
|
||||||
|
cnd_t* condition_not_empty, cnd_t* condition_not_full,
|
||||||
|
const bool* cancelled) {
|
||||||
|
mtx_lock(mutex);
|
||||||
|
while (queue_is_full(queue) && (cancelled == NULL || !*cancelled))
|
||||||
|
cnd_wait(condition_not_full, mutex);
|
||||||
|
if (cancelled != NULL && *cancelled) {
|
||||||
|
mtx_unlock(mutex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok = queue_enqueue(queue, item);
|
||||||
|
cnd_signal(condition_not_empty);
|
||||||
|
mtx_unlock(mutex);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
void* queue_dequeue(Queue* queue) {
|
void* queue_dequeue(Queue* queue) {
|
||||||
if (queue == NULL || queue_is_empty(queue)) {
|
if (queue == NULL || queue_is_empty(queue)) {
|
||||||
perror("ERROR: Could not dequeue from null or empty queue.");
|
perror("ERROR: Could not dequeue from null or empty queue.");
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ bool queue_is_full(const Queue* queue);
|
|||||||
bool queue_enqueue(Queue* queue, void* item);
|
bool queue_enqueue(Queue* queue, void* item);
|
||||||
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
|
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||||
cnd_t* condition_not_full);
|
cnd_t* condition_not_full);
|
||||||
|
bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex,
|
||||||
|
cnd_t* condition_not_empty, cnd_t* condition_not_full,
|
||||||
|
const bool* cancelled);
|
||||||
void* queue_dequeue(Queue* queue);
|
void* queue_dequeue(Queue* queue);
|
||||||
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
|
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||||
cnd_t* condition_not_full, const bool* other_thread_done);
|
cnd_t* condition_not_full, const bool* other_thread_done);
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
static volatile sig_atomic_t g_active_connections = 0;
|
static volatile sig_atomic_t g_active_connections = 0;
|
||||||
|
|
||||||
|
static void tcp_apply_socket_timeout(int fd);
|
||||||
|
|
||||||
static void sigchld_handler(int sig) {
|
static void sigchld_handler(int sig) {
|
||||||
(void)sig;
|
(void)sig;
|
||||||
int saved_errno = errno;
|
int saved_errno = errno;
|
||||||
@@ -93,6 +95,7 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
|||||||
perror("Could not accept the connection");
|
perror("Could not accept the connection");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
tcp_apply_socket_timeout(fd);
|
||||||
if ((unsigned int)g_active_connections >= server->max_connections) {
|
if ((unsigned int)g_active_connections >= server->max_connections) {
|
||||||
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting",
|
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting",
|
||||||
server->max_connections);
|
server->max_connections);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
|||||||
SSL_CTX_free(ctx);
|
SSL_CTX_free(ctx);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
|
||||||
SSL_CTX_set_verify_depth(ctx, 4);
|
SSL_CTX_set_verify_depth(ctx, 4);
|
||||||
} else {
|
} else {
|
||||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
|
||||||
|
|||||||
@@ -193,6 +193,26 @@ class TestIncremental:
|
|||||||
content = f.read()
|
content = f.read()
|
||||||
assert b"modified content" in content, f"Modified content not transferred: {content[:50]}"
|
assert b"modified content" in content, f"Modified content not transferred: {content[:50]}"
|
||||||
|
|
||||||
|
def test_checksum_detects_same_size_and_mtime_change(self, shared_server):
|
||||||
|
clean_dir(DEST_DIR)
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||||
|
source_file = os.path.join(SOURCE_DIR, "small.txt")
|
||||||
|
received_file = os.path.join(received, "small.txt")
|
||||||
|
source_stat = os.stat(source_file)
|
||||||
|
with open(received_file, "wb") as f:
|
||||||
|
f.write(b"different!\n")
|
||||||
|
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||||
|
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||||
|
flags=["-M", "--incremental", "--checksum"],
|
||||||
|
port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Checksum sync failed: {result.stderr[:200]}"
|
||||||
|
with open(received_file, "rb") as f:
|
||||||
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
|
||||||
class TestDelete:
|
class TestDelete:
|
||||||
def test_delete_removes_extra_files(self, shared_server):
|
def test_delete_removes_extra_files(self, shared_server):
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ static void test_xxhash32_different_data() {
|
|||||||
EXPECT_TRUE(ha != hb);
|
EXPECT_TRUE(ha != hb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_xxhash64_different_data() {
|
||||||
|
EXPECT_TRUE(delta_xxhash64("AAAA", 4) != delta_xxhash64("BBBB", 4));
|
||||||
|
}
|
||||||
|
|
||||||
static void test_signature_roundtrip() {
|
static void test_signature_roundtrip() {
|
||||||
char old_data[4096];
|
char old_data[4096];
|
||||||
for (int i = 0; i < 4096; i++)
|
for (int i = 0; i < 4096; i++)
|
||||||
@@ -328,6 +332,7 @@ void test_delta() {
|
|||||||
test_adler32_different_data();
|
test_adler32_different_data();
|
||||||
test_xxhash32_basic();
|
test_xxhash32_basic();
|
||||||
test_xxhash32_different_data();
|
test_xxhash32_different_data();
|
||||||
|
test_xxhash64_different_data();
|
||||||
test_signature_roundtrip();
|
test_signature_roundtrip();
|
||||||
test_delta_identical_files();
|
test_delta_identical_files();
|
||||||
test_delta_small_edit();
|
test_delta_small_edit();
|
||||||
|
|||||||
@@ -126,6 +126,28 @@ static void test_to_disk_creates_dirs() {
|
|||||||
rmdir("test_nested_tmp");
|
rmdir("test_nested_tmp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_to_disk_does_not_follow_symlink() {
|
||||||
|
const char* outside = "test_to_disk_outside.txt";
|
||||||
|
const char* link = "test_to_disk_link.txt";
|
||||||
|
const char* content = "confined";
|
||||||
|
unlink(outside);
|
||||||
|
unlink(link);
|
||||||
|
EXPECT_TRUE(to_disk(outside, "outside", 7, false, false));
|
||||||
|
EXPECT_EQ_INT(symlink(outside, link), 0);
|
||||||
|
EXPECT_TRUE(to_disk(link, content, strlen(content), false, false));
|
||||||
|
FILE* fp = fopen(outside, "rb");
|
||||||
|
char buf[16] = {0};
|
||||||
|
EXPECT_NOT_NULL(fp);
|
||||||
|
if (fp) {
|
||||||
|
size_t read_count = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||||
|
EXPECT_TRUE(read_count <= sizeof(buf) - 1);
|
||||||
|
fclose(fp);
|
||||||
|
}
|
||||||
|
EXPECT_EQ_STR(buf, "outside");
|
||||||
|
unlink(outside);
|
||||||
|
unlink(link);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_file_content_to_buffer() {
|
static void test_file_content_to_buffer() {
|
||||||
const char* content = "Buffer content test";
|
const char* content = "Buffer content test";
|
||||||
EXPECT_TRUE(to_disk("test_buffer_file.txt", content, strlen(content), false, false));
|
EXPECT_TRUE(to_disk("test_buffer_file.txt", content, strlen(content), false, false));
|
||||||
@@ -434,6 +456,7 @@ void test_file() {
|
|||||||
test_file_save_to_disk();
|
test_file_save_to_disk();
|
||||||
test_to_disk_basic();
|
test_to_disk_basic();
|
||||||
test_to_disk_creates_dirs();
|
test_to_disk_creates_dirs();
|
||||||
|
test_to_disk_does_not_follow_symlink();
|
||||||
test_file_content_to_buffer();
|
test_file_content_to_buffer();
|
||||||
test_file_save_to_disk_path_traversal();
|
test_file_save_to_disk_path_traversal();
|
||||||
test_file_save_to_disk_deep_traversal();
|
test_file_save_to_disk_deep_traversal();
|
||||||
|
|||||||
@@ -109,6 +109,19 @@ static void test_metadata_send_null() {
|
|||||||
close(p[1]);
|
close(p[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_metadata_rejects_invalid_values() {
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
|
io_set_fds(p[0], p[1]);
|
||||||
|
int32_t present = 2;
|
||||||
|
EXPECT_TRUE(send_n_data(p[1], &present, sizeof(present)));
|
||||||
|
int ok = 1;
|
||||||
|
EXPECT_NULL(metadata_receive(p[0], &ok));
|
||||||
|
EXPECT_EQ_INT(ok, 0);
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
}
|
||||||
|
|
||||||
static void test_file_restore_metadata() {
|
static void test_file_restore_metadata() {
|
||||||
const char* path = "temp_meta_restore_test.txt";
|
const char* path = "temp_meta_restore_test.txt";
|
||||||
const char* content = "test content";
|
const char* content = "test content";
|
||||||
@@ -137,5 +150,6 @@ void test_metadata() {
|
|||||||
test_metadata_from_buf_null();
|
test_metadata_from_buf_null();
|
||||||
test_metadata_send_receive_roundtrip();
|
test_metadata_send_receive_roundtrip();
|
||||||
test_metadata_send_null();
|
test_metadata_send_null();
|
||||||
|
test_metadata_rejects_invalid_values();
|
||||||
test_file_restore_metadata();
|
test_file_restore_metadata();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ static void test_sender_queue_capacities() {
|
|||||||
pipeline_context_sender_destroy(ctx);
|
pipeline_context_sender_destroy(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Test that create handles zero-capacity queues */
|
/* Invalid queue capacities must not create unusable pipeline queues. */
|
||||||
static void test_sender_zero_capacity() {
|
static void test_sender_zero_capacity() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
EXPECT_NOT_NULL(cfg);
|
EXPECT_NOT_NULL(cfg);
|
||||||
@@ -93,11 +93,9 @@ static void test_sender_zero_capacity() {
|
|||||||
|
|
||||||
Queue* q1 = queue_create(0, NULL);
|
Queue* q1 = queue_create(0, NULL);
|
||||||
Queue* q2 = queue_create(0, NULL);
|
Queue* q2 = queue_create(0, NULL);
|
||||||
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2);
|
EXPECT_NULL(q1);
|
||||||
EXPECT_NOT_NULL(ctx);
|
EXPECT_NULL(q2);
|
||||||
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 0);
|
config_delete(cfg);
|
||||||
EXPECT_EQ_INT(ctx->queue_loader->capacity, 0);
|
|
||||||
pipeline_context_sender_destroy(ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Test receiver with zero file_descriptor */
|
/* Test receiver with zero file_descriptor */
|
||||||
@@ -168,6 +166,41 @@ static void test_receive_thread_finished() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A malformed terminal status must wake a writer waiting on an empty queue. */
|
||||||
|
static void test_receive_thread_failure_wakes_writer() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
EXPECT_NOT_NULL(cfg);
|
||||||
|
free(cfg->version);
|
||||||
|
cfg->version = str_dup(PROTOCOL_VERSION);
|
||||||
|
cfg->send_directory = str_dup("/src");
|
||||||
|
cfg->receive_root_directory = str_dup("/tmp/dst");
|
||||||
|
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(pipe(p), 0);
|
||||||
|
Queue* q = queue_create(1, file_destroy);
|
||||||
|
EXPECT_NOT_NULL(q);
|
||||||
|
PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, p[0], NULL);
|
||||||
|
EXPECT_NOT_NULL(ctx);
|
||||||
|
|
||||||
|
thrd_t receiver;
|
||||||
|
thrd_t writer;
|
||||||
|
EXPECT_EQ_INT(thrd_create(&writer, write_thread, ctx), thrd_success);
|
||||||
|
EXPECT_EQ_INT(thrd_create(&receiver, receive_thread, ctx), thrd_success);
|
||||||
|
EXPECT_TRUE(send_status(p[1], STATUS_OK));
|
||||||
|
close(p[1]);
|
||||||
|
|
||||||
|
int receiver_result;
|
||||||
|
int writer_result;
|
||||||
|
EXPECT_EQ_INT(thrd_join(receiver, &receiver_result), thrd_success);
|
||||||
|
EXPECT_EQ_INT(thrd_join(writer, &writer_result), thrd_success);
|
||||||
|
EXPECT_EQ_INT(receiver_result, thrd_error);
|
||||||
|
EXPECT_EQ_INT(writer_result, thrd_success);
|
||||||
|
EXPECT_TRUE(ctx->receiver_done);
|
||||||
|
|
||||||
|
close(p[0]);
|
||||||
|
pipeline_context_receiver_destroy(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
/* Test that write_thread completes cleanly when queue signals done */
|
/* Test that write_thread completes cleanly when queue signals done */
|
||||||
static void test_write_thread_done() {
|
static void test_write_thread_done() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
@@ -223,6 +256,7 @@ void test_multiprocessing() {
|
|||||||
test_receiver_fd_zero();
|
test_receiver_fd_zero();
|
||||||
if (!is_running_under_valgrind()) {
|
if (!is_running_under_valgrind()) {
|
||||||
test_receive_thread_finished();
|
test_receive_thread_finished();
|
||||||
|
test_receive_thread_failure_wakes_writer();
|
||||||
}
|
}
|
||||||
test_write_thread_done();
|
test_write_thread_done();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ static void test_queue_basic() {
|
|||||||
queue_destroy(q);
|
queue_destroy(q);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_queue_rejects_invalid_capacity() {
|
||||||
|
EXPECT_NULL(queue_create(0, NULL));
|
||||||
|
EXPECT_NULL(queue_create(-1, NULL));
|
||||||
|
}
|
||||||
|
|
||||||
static void test_queue_resize() {
|
static void test_queue_resize() {
|
||||||
Queue* q = queue_create(3, NULL);
|
Queue* q = queue_create(3, NULL);
|
||||||
EXPECT_NOT_NULL(q);
|
EXPECT_NOT_NULL(q);
|
||||||
@@ -199,6 +204,7 @@ static void test_queue_multithreaded() {
|
|||||||
|
|
||||||
void test_queue() {
|
void test_queue() {
|
||||||
test_queue_basic();
|
test_queue_basic();
|
||||||
|
test_queue_rejects_invalid_capacity();
|
||||||
test_queue_resize();
|
test_queue_resize();
|
||||||
test_queue_destroyer();
|
test_queue_destroyer();
|
||||||
test_queue_multithreaded();
|
test_queue_multithreaded();
|
||||||
|
|||||||
@@ -171,10 +171,26 @@ static void test_receive_files_abort() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_receive_manifest_rejects_traversal() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
EXPECT_NOT_NULL(cfg);
|
||||||
|
cfg->receive_root_directory = str_dup("/tmp/dst");
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
|
io_set_fds(p[0], p[1]);
|
||||||
|
EXPECT_TRUE(send_int(p[1], 1));
|
||||||
|
EXPECT_TRUE(send_str(p[1], "../outside"));
|
||||||
|
EXPECT_EQ_INT(receive_manifest(p[0], cfg, NULL), -1);
|
||||||
|
close(p[0]);
|
||||||
|
close(p[1]);
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
void test_server() {
|
void test_server() {
|
||||||
if (!is_running_under_valgrind()) {
|
if (!is_running_under_valgrind()) {
|
||||||
test_receive_files_finished();
|
test_receive_files_finished();
|
||||||
test_receive_files_single_file();
|
test_receive_files_single_file();
|
||||||
test_receive_files_abort();
|
test_receive_files_abort();
|
||||||
|
test_receive_manifest_rejects_traversal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user