Author SHA1 Message Date
TapTap 5c055960b8 docs: correct compatibility summary
CI / lint (pull_request) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 36s
CI / sanitizers (undefined) (pull_request) Successful in 35s
CI / fuzz-build (pull_request) Successful in 15s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-09-01 21:18:23 +02:00
TapTap 405e5b3b00 docs: document missing rsync flags 2026-09-01 21:18:08 +02:00
13 changed files with 47 additions and 234 deletions

No files matched your search

+1 -1
View File
@@ -465,7 +465,7 @@ defaults to the current directory. |
## Protocol and Security ## Protocol and Security
FastSync protocol version `2.3.0` is shared by the client and server. The FastSync protocol version `2.2.0` is shared by the client and server. The
current protocol is sender-driven and includes configuration negotiation, current protocol is sender-driven and includes configuration negotiation,
incremental checks, checksums, manifests, keep-alives, abort handling, and incremental checks, checksums, manifests, keep-alives, abort handling, and
FastSync-native delta messages. Client and server versions must currently FastSync-native delta messages. Client and server versions must currently
+25 -12
View File
@@ -8,9 +8,9 @@ This document maps rsync's full feature set to FastSync's current implementation
|--------|-------|-------------| |--------|-------|-------------|
| ✅ Implemented | 34 | Feature works end-to-end | | ✅ Implemented | 34 | Feature works end-to-end |
| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics | | 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 1 | Flag parsed/stored but behavior incomplete | | ⚠️ Partial | 3 | Flag parsed/stored but behavior incomplete |
| ❌ Not Implemented | 98 | Flag not recognized or no behavior | | ❌ Not Implemented | 110 | Flag not recognized or no behavior |
| **Total** | **136** | | | **Total** | **150** | |
--- ---
@@ -30,6 +30,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner | | `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner |
| `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner | | `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner |
| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ❌ Not Implemented | Removed because it had no effect | | `-C`, `--cvs-exclude` | Auto-ignore CVS files | ❌ Not Implemented | Removed because it had no effect |
| `--no-OPTION` | Turn off an implied option | ❌ Not Implemented | Rsync supports this for options such as `--no-D`; FastSync does not |
## 2. Modifying Output ## 2. Modifying Output
@@ -43,7 +44,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--out-format=FORMAT` | Custom output format | ❌ Not Implemented | Removed because it had no effect | | `--out-format=FORMAT` | Custom output format | ❌ Not Implemented | Removed because it had no effect |
| `--log-file=FILE` | Log to file | ✅ Implemented | `log_file` config field | | `--log-file=FILE` | Log to file | ✅ Implemented | `log_file` config field |
| `--log-file-format=FMT` | Log format | ❌ Not Implemented | | | `--log-file-format=FMT` | Log format | ❌ Not Implemented | |
| `--8-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | | | `-8`, `--8-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | |
| `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect | | `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect |
## 3. File Selection ## 3. File Selection
@@ -52,7 +53,8 @@ This document maps rsync's full feature set to FastSync's current implementation
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--exclude-from=FILE` | Read exclude patterns from file | ✅ Implemented | Reads patterns from file | | `--exclude-from=FILE` | Read exclude patterns from file | ✅ Implemented | Reads patterns from file |
| `--include-from=FILE` | Read include patterns from file | ✅ Implemented | Reads patterns from file | | `--include-from=FILE` | Read include patterns from file | ✅ Implemented | Reads patterns from file |
| `--filter=RULE` | Add file-filtering rule | ❌ Not Implemented | Removed because it had no effect | | `-f`, `--filter=RULE` | Add a file-filtering rule | ❌ Not Implemented | `-f` is FastSync's sendfile flag |
| `-F` | Add the default `.rsync-filter` rules | ❌ Not Implemented | |
| `--files-from=FILE` | Read source file list from file | ❌ Not Implemented | Removed because it had no effect | | `--files-from=FILE` | Read source file list from file | ❌ Not Implemented | Removed because it had no effect |
| `-0`, `--from0` | Delimit *-from files with NULs | ❌ Not Implemented | | | `-0`, `--from0` | Delimit *-from files with NULs | ❌ Not Implemented | |
| `--max-size=SIZE` | Skip files larger than SIZE | ✅ Implemented | `max_size` in scanner | | `--max-size=SIZE` | Skip files larger than SIZE | ✅ Implemented | `max_size` in scanner |
@@ -63,6 +65,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--existing` | Skip creating new files on receiver | ❌ Not Implemented | | | `--existing` | Skip creating new files on receiver | ❌ Not Implemented | |
| `--ignore-existing` | Skip updating existing files | ❌ Not Implemented | | | `--ignore-existing` | Skip updating existing files | ❌ Not Implemented | |
| `--remove-source-files` | Sender removes synced files | ❌ Not Implemented | | | `--remove-source-files` | Sender removes synced files | ❌ Not Implemented | |
| `-x`, `--one-file-system` | Do not cross filesystem boundaries | ❌ Not Implemented | |
## 4. Directory Options ## 4. Directory Options
@@ -72,6 +75,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `-R`, `--relative` | Use relative path names | ❌ Not Implemented | Removed because it had no effect | | `-R`, `--relative` | Use relative path names | ❌ Not Implemented | Removed because it had no effect |
| `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | | | `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | |
| `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | | | `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | |
| `--old-dirs`, `--old-d` | Transfer directories without recursing | ❌ Not Implemented | Compatibility aliases for `--dirs` |
| `--mkpath` | Create missing path components | ❌ Not Implemented | | | `--mkpath` | Create missing path components | ❌ Not Implemented | |
## 5. Transfer Modifications ## 5. Transfer Modifications
@@ -83,7 +87,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--append` | Append data to shorter files | ❌ Not Implemented | Removed because it had no effect | | `--append` | Append data to shorter files | ❌ Not Implemented | Removed because it had no effect |
| `--append-verify` | Append with old-data checksum | ❌ Not Implemented | Removed because it had no effect | | `--append-verify` | Append with old-data checksum | ❌ Not Implemented | Removed because it had no effect |
| `-W`, `--whole-file` | Copy whole file (no delta) | ❌ Not Implemented | | | `-W`, `--whole-file` | Copy whole file (no delta) | ❌ Not Implemented | |
| `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | Parsed as `--delta-block`; controls delta transfer block size | | `-B`, `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | Parsed as `--delta-block`; controls delta transfer block size |
## 6. Destination Handling ## 6. Destination Handling
@@ -94,12 +98,14 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--backup-dir=DIR` | Backup directory hierarchy | ✅ Implemented | `backup_dir` config field | | `--backup-dir=DIR` | Backup directory hierarchy | ✅ Implemented | `backup_dir` config field |
| `--suffix=SUFFIX` | Backup suffix (default ~) | ✅ Implemented | `suffix` config field | | `--suffix=SUFFIX` | Backup suffix (default ~) | ✅ Implemented | `suffix` config field |
| `--delay-updates` | Put updated files in place at end | ❌ Not Implemented | | | `--delay-updates` | Put updated files in place at end | ❌ Not Implemented | |
| `-T`, `--temp-dir=DIR` | Create temporary files in DIR | ❌ Not Implemented | `-T` is FastSync's timeout alias |
## 7. Deletion ## 7. Deletion
| Flag | Rsync Description | FastSync Status | Notes | | Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field | | `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field |
| `--del` | Delete during transfer | ❌ Not Implemented | Alias for `--delete-during` |
| `--delete-before` | Delete before transfer | ❌ Not Implemented | Removed because it had no effect | | `--delete-before` | Delete before transfer | ❌ Not Implemented | Removed because it had no effect |
| `--delete-during` | Delete during transfer | ❌ Not Implemented | | | `--delete-during` | Delete during transfer | ❌ Not Implemented | |
| `--delete-delay` | Find deletions during, delete after | ❌ Not Implemented | | | `--delete-delay` | Find deletions during, delete after | ❌ Not Implemented | |
@@ -108,7 +114,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--max-delete=NUM` | Max files to delete | ❌ Not Implemented | Removed because it had no effect | | `--max-delete=NUM` | Max files to delete | ❌ Not Implemented | Removed because it had no effect |
| `--ignore-errors` | Delete even with I/O errors | ❌ Not Implemented | | | `--ignore-errors` | Delete even with I/O errors | ❌ Not Implemented | |
| `--force` | Force deletion of non-empty dirs | ❌ Not Implemented | | | `--force` | Force deletion of non-empty dirs | ❌ Not Implemented | |
| `--prune-empty-dirs` | Prune empty dir chains | ❌ Not Implemented | Removed because it had no effect | | `-m`, `--prune-empty-dirs` | Prune empty dir chains | ❌ Not Implemented | `-m` enables FastSync multithreading instead |
## 8. Metadata Preservation ## 8. Metadata Preservation
@@ -135,6 +141,12 @@ This document maps rsync's full feature set to FastSync's current implementation
| `-J`, `--omit-link-times` | Omit symlinks from --times | ❌ Not Implemented | | | `-J`, `--omit-link-times` | Omit symlinks from --times | ❌ Not Implemented | |
| `--super` | Receiver attempts super-user activities | ❌ Not Implemented | | | `--super` | Receiver attempts super-user activities | ❌ Not Implemented | |
| `--fake-super` | Store/recover privileged attrs via xattrs | ❌ Not Implemented | | | `--fake-super` | Store/recover privileged attrs via xattrs | ❌ Not Implemented | |
| `--open-noatime` | Avoid changing access time when opening files | ❌ Not Implemented | |
| `--numeric-ids` | Do not map uid/gid by name | ❌ Not Implemented | |
| `--usermap=STRING` | Map usernames | ❌ Not Implemented | |
| `--groupmap=STRING` | Map group names | ❌ Not Implemented | |
| `--chown=USER:GROUP` | Map owner and group | ❌ Not Implemented | |
| `--copy-as=USER[:GROUP]` | Perform the copy as another user/group | ❌ Not Implemented | |
## 9. Symlink Handling ## 9. Symlink Handling
@@ -160,19 +172,19 @@ This document maps rsync's full feature set to FastSync's current implementation
| Flag | Rsync Description | FastSync Status | Notes | | Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--checksum` | Skip based on checksum | ✅ Implemented | With `--incremental`, compares xxHash64 content checksums; `-c` remains compression | | `--checksum` | Skip based on checksum | ✅ Implemented | With `--incremental`, compares xxHash64 content checksums; `-c` remains compression |
| `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally | | `--checksum-choice=STR` (`--cc`) | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally |
| `--compare-dest=DIR` | Compare dest files relative to DIR | ❌ Not Implemented | Removed because it had no effect | | `--compare-dest=DIR` | Compare dest files relative to DIR | ❌ Not Implemented | Removed because it had no effect |
| `--copy-dest=DIR` | Include copies of unchanged files | ❌ Not Implemented | Removed because it had no effect | | `--copy-dest=DIR` | Include copies of unchanged files | ❌ Not Implemented | Removed because it had no effect |
| `--link-dest=DIR` | Hardlink to files when unchanged | ❌ Not Implemented | Removed because it had no effect | | `--link-dest=DIR` | Hardlink to files when unchanged | ❌ Not Implemented | Removed because it had no effect |
| `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | | | `-y`, `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | `--no-fuzzy` has no short alias |
## 12. Compression ## 12. Compression
| Flag | Rsync Description | FastSync Status | Notes | | Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) | | `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) |
| `--compress-choice=STR` | Choose compression algorithm | ❌ Not Implemented | Removed because it had no effect; FastSync always uses zstd | | `--compress-choice=STR` (`--zc`) | Choose compression algorithm | ❌ Not Implemented | Removed because it had no effect; FastSync always uses zstd |
| `--compress-level=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 | | `--compress-level=NUM` (`--zl`) | Set compression level | ✅ Implemented | 1-22, default 5 |
| `--compress-threads=NUM` | Set compression threads | ❌ Not Implemented | | | `--compress-threads=NUM` | Set compression threads | ❌ Not Implemented | |
| `--skip-compress=LIST` | Skip compress for suffixes | ❌ Not Implemented | Internal skip for hardcoded types; not user-configurable | | `--skip-compress=LIST` | Skip compress for suffixes | ❌ Not Implemented | Internal skip for hardcoded types; not user-configurable |
@@ -189,6 +201,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect | | `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect |
| `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect | | `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect |
| `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect | | `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect |
| `--remote-option=OPT`, `-M` | Send an option only to the remote side | ❌ Not Implemented | `-M` is FastSync's metadata-preservation flag |
## 14. Daemon Mode ## 14. Daemon Mode
@@ -214,6 +227,7 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--old-args` | Disable modern arg protection | ❌ Not Implemented | | | `--old-args` | Disable modern arg protection | ❌ Not Implemented | |
| `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | | | `--ignore-missing-args` | Ignore missing source args | ❌ Not Implemented | |
| `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | | | `--delete-missing-args` | Delete missing source args | ❌ Not Implemented | |
| `--max-alloc=SIZE` | Limit a single memory allocation | ❌ Not Implemented | |
## 16. Batch Operations ## 16. Batch Operations
@@ -234,7 +248,6 @@ This document maps rsync's full feature set to FastSync's current implementation
| `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | | | `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | |
| `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | | | `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | |
| `-s`, `--secluded-args` | Use protocol to send args | ❌ Not Implemented | | | `-s`, `--secluded-args` | Use protocol to send args | ❌ Not Implemented | |
| `--no-OPTION` | Turn off implied option | ❌ Not Implemented | |
--- ---
-1
View File
@@ -153,7 +153,6 @@ static const OptionEntry OPTION_TABLE[] = {
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)}, {"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)}, {"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)}, {"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
{"--ignore-existing", NULL, OPT_FLAG, offsetof(Config, ignore_existing)},
{"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)}, {"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)}, {"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
-1
View File
@@ -21,7 +21,6 @@ void print_usage(void) {
printf(" -p <port> SSH port (default: 22)\n"); printf(" -p <port> SSH port (default: 22)\n");
printf(" --progress Show transfer progress\n"); printf(" --progress Show transfer progress\n");
printf(" --delete Delete files on receiver not in source\n"); printf(" --delete Delete files on receiver not in source\n");
printf(" --ignore-existing Skip files that already exist on receiver\n");
printf(" --exclude <pattern> Exclude files matching pattern\n"); printf(" --exclude <pattern> Exclude files matching pattern\n");
printf(" --include <pattern> Only include files matching pattern\n"); printf(" --include <pattern> Only include files matching pattern\n");
printf(" --exclude-from <file> Read exclude patterns from file\n"); printf(" --exclude-from <file> Read exclude patterns from file\n");
+11 -14
View File
@@ -68,7 +68,6 @@ static void config_set_defaults(Config* config) {
config->debug_level = 0; config->debug_level = 0;
config->list_only = false; config->list_only = false;
config->human_readable = false; config->human_readable = false;
config->ignore_existing = false;
config->update = false; config->update = false;
config->inplace = false; config->inplace = false;
config->append = false; config->append = false;
@@ -124,13 +123,12 @@ static bool validate_received_config(const Config* config) {
valid_wire_bool(config->copy_unsafe_links) && valid_wire_bool(config->copy_unsafe_links) &&
valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) && valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) &&
valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) && valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) &&
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->ignore_existing) && valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->update) &&
valid_wire_bool(config->update) && valid_wire_bool(config->inplace) && valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
valid_wire_bool(config->append) && valid_wire_bool(config->append_verify) && valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) &&
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) && valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) &&
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) &&
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) && valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) &&
valid_wire_bool(config->checksum) &&
(!config->use_compression || (!config->use_compression ||
(config->compression_level >= 1 && config->compression_level <= 22)) && (config->compression_level >= 1 && config->compression_level <= 22)) &&
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
@@ -245,11 +243,10 @@ static bool send_file_options(int fd, const Config* c) {
} }
static bool send_selection_options(int fd, const Config* c) { static bool send_selection_options(int fd, const Config* c) {
return send_int(fd, c->ignore_existing) && send_int(fd, c->update) && send_int(fd, c->inplace) && return send_int(fd, c->update) && send_int(fd, c->inplace) && send_int(fd, c->append) &&
send_int(fd, c->append) && send_int(fd, c->append_verify) && send_int(fd, c->append_verify) && send_int(fd, c->delete_excluded) &&
send_int(fd, c->delete_excluded) && send_int(fd, c->delete_after) && send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) &&
send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && send_int(fd, c->relative) && send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs);
send_int(fd, c->prune_empty_dirs);
} }
static bool send_resume_options(int fd, const Config* c) { static bool send_resume_options(int fd, const Config* c) {
@@ -307,7 +304,7 @@ static bool receive_file_options(int fd, Config* c) {
} }
static bool receive_selection_options(int fd, Config* c) { static bool receive_selection_options(int fd, Config* c) {
bool* flags[] = {&c->ignore_existing, &c->update, &c->inplace, &c->append, bool* flags[] = {&c->update, &c->inplace, &c->append,
&c->append_verify, &c->delete_excluded, &c->delete_after}; &c->append_verify, &c->delete_excluded, &c->delete_after};
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
if (!receive_wire_bool(fd, flags[i])) if (!receive_wire_bool(fd, flags[i]))
+1 -2
View File
@@ -76,7 +76,6 @@ typedef struct Config {
bool human_readable; bool human_readable;
// Issue #127: Transfer modes // Issue #127: Transfer modes
bool ignore_existing;
bool update; bool update;
bool inplace; bool inplace;
bool append; bool append;
@@ -129,7 +128,7 @@ typedef struct Config {
char* compress_choice; char* compress_choice;
} Config; } Config;
#define PROTOCOL_VERSION "2.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);
+4 -37
View File
@@ -172,17 +172,8 @@ bool file_set_authorized_root(int fd, const char* canonical_path) {
} }
bool file_path_exists_secure(const char* path) { bool file_path_exists_secure(const char* path) {
if (!path)
return false;
char* leaf = NULL;
int parent_fd = file_open_secure_parent(path, &leaf, false);
if (parent_fd < 0)
return false;
struct stat st; struct stat st;
bool exists = fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) == 0; return file_stat_secure(path, &st);
close(parent_fd);
free(leaf);
return exists;
} }
bool file_stat_secure(const char* path, struct stat* st) { bool file_stat_secure(const char* path, struct stat* st) {
@@ -311,9 +302,8 @@ bool file_rename_secure(const char* old_path, const char* new_path) {
return ok; return ok;
} }
static bool file_to_disk_secure_impl(const char* path, const void* data, bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
unsigned long long data_size, bool inplace, bool sparse, bool inplace, bool sparse, const FileMetadata* metadata) {
const FileMetadata* metadata, bool no_replace) {
char* leaf = NULL; char* leaf = NULL;
int dirfd = file_open_secure_parent(path, &leaf, true); int dirfd = file_open_secure_parent(path, &leaf, true);
if (dirfd < 0) if (dirfd < 0)
@@ -344,20 +334,8 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
if (close(fd) != 0) if (close(fd) != 0)
ok = false; ok = false;
fd = -1; fd = -1;
if (ok) { if (ok && renameat(dirfd, tmp, dirfd, leaf) != 0)
if (no_replace) {
/* The probe and commit cannot be one operation. A concurrent
creator may win; EEXIST is then the requested skip. */
if (linkat(dirfd, tmp, dirfd, leaf, 0) == 0 || errno == EEXIST) {
if (unlinkat(dirfd, tmp, 0) != 0 && errno != ENOENT)
ok = false; ok = false;
} else {
ok = false;
}
} else if (renameat(dirfd, tmp, dirfd, leaf) != 0) {
ok = false;
}
}
if (!ok) if (!ok)
unlinkat(dirfd, tmp, 0); unlinkat(dirfd, tmp, 0);
} }
@@ -369,17 +347,6 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
return ok; return ok;
} }
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse, const FileMetadata* metadata) {
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, metadata, false);
}
bool file_to_disk_secure_no_replace(const char* path, const void* data,
unsigned long long data_size, bool sparse,
const FileMetadata* metadata) {
return file_to_disk_secure_impl(path, data, data_size, false, sparse, metadata, true);
}
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse) { bool inplace, bool sparse) {
if (!path || (!data && data_size != 0) || has_path_traversal(path)) if (!path || (!data && data_size != 0) || has_path_traversal(path))
-3
View File
@@ -32,8 +32,5 @@ bool file_ensure_directory_secure(const char* path);
bool file_rename_secure(const char* old_path, const char* new_path); bool file_rename_secure(const char* old_path, const char* new_path);
bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse, const FileMetadata* metadata); bool inplace, bool sparse, const FileMetadata* metadata);
bool file_to_disk_secure_no_replace(const char* path, const void* data,
unsigned long long data_size, bool sparse,
const FileMetadata* metadata);
#endif #endif
+3 -22
View File
@@ -22,9 +22,7 @@
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE #define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) { bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
/* Backups are incompatible with ignore-existing: moving the entry first bool backup_enabled = config && config->backup;
would make a concurrent no-replace commit overwrite its old name. */
bool backup_enabled = config && config->backup && !config->ignore_existing;
bool inplace = config && config->inplace; bool inplace = config && config->inplace;
bool sparse = config && config->preserve_sparse; bool sparse = config && config->preserve_sparse;
const char* backup_suffix = (config && config->suffix) ? config->suffix : "~"; const char* backup_suffix = (config && config->suffix) ? config->suffix : "~";
@@ -63,20 +61,6 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
return false; return false;
} }
/* --ignore-existing checks the final destination before partial files or
overwrite policies can modify it. */
if (config && config->ignore_existing) {
char* final_path = path_cat(root_directory, file->path);
bool exists = final_path && file_path_exists_secure(final_path);
free(final_path);
if (exists) {
free(confined_backup);
free(confined_partial);
free(disk_path);
return true;
}
}
/* --update is receiver-side policy: never replace a newer destination. */ /* --update is receiver-side policy: never replace a newer destination. */
if (config && config->update) { if (config && config->update) {
struct stat destination_stat; struct stat destination_stat;
@@ -119,11 +103,8 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
} }
} }
bool ok = config && config->ignore_existing bool ok = file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse,
? file_to_disk_secure_no_replace(disk_path, file->data->data, file->data->size, file->metadata);
sparse, file->metadata)
: file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace,
sparse, file->metadata);
free(parent_copy); free(parent_copy);
free(backup_path); free(backup_path);
free(confined_backup); free(confined_backup);
-27
View File
@@ -214,33 +214,6 @@ class TestIncremental:
assert f.read() == b"hello world\n" assert f.read() == b"hello world\n"
class TestIgnoreExisting:
def test_ignore_existing_preserves_existing_and_transfers_new(self, shared_server):
clean_dir(DEST_DIR)
result, _ = run_client(SOURCE_DIR, DEST_DIR, port=shared_server.port)
assert result.returncode == 0
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
existing_file = os.path.join(received, "small.txt")
with open(existing_file, "wb") as f:
f.write(b"destination content\n")
new_source = os.path.join(SOURCE_DIR, "new.txt")
try:
with open(new_source, "wb") as f:
f.write(b"new file\n")
result, _ = run_client(SOURCE_DIR, DEST_DIR,
flags=["--ignore-existing"], port=shared_server.port)
assert result.returncode == 0, f"Sync failed: {(result.stderr or result.stdout)[:200]}"
with open(existing_file, "rb") as f:
assert f.read() == b"destination content\n"
with open(os.path.join(received, "new.txt"), "rb") as f:
assert f.read() == b"new file\n"
finally:
if os.path.lexists(new_source):
os.unlink(new_source)
class TestDelete: class TestDelete:
def test_delete_removes_extra_files(self, shared_server): def test_delete_removes_extra_files(self, shared_server):
clean_dir(DEST_DIR) clean_dir(DEST_DIR)
-14
View File
@@ -181,19 +181,6 @@ static void test_parse_args_valid_port() {
config_delete(cfg); config_delete(cfg);
} }
static void test_parse_args_ignore_existing() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "--ignore-existing", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
int ret = parse_args(cfg, 4, argv, positional_args, &positional_count);
EXPECT_EQ_INT(ret, 0);
EXPECT_TRUE(cfg->ignore_existing);
config_delete(cfg);
}
/* Test parse_args rejects port > 65535 */ /* Test parse_args rejects port > 65535 */
static void test_parse_args_invalid_port() { static void test_parse_args_invalid_port() {
Config* cfg = config_create(); Config* cfg = config_create();
@@ -365,7 +352,6 @@ void test_client_cli() {
test_parse_args_help(); test_parse_args_help();
test_parse_args_version(); test_parse_args_version();
test_parse_args_valid_port(); test_parse_args_valid_port();
test_parse_args_ignore_existing();
test_parse_args_invalid_port(); test_parse_args_invalid_port();
test_parse_args_non_numeric_port(); test_parse_args_non_numeric_port();
test_parse_args_invalid_server_port(); test_parse_args_invalid_server_port();
+2 -5
View File
@@ -126,7 +126,6 @@ static void test_config_send_receive() {
send_cfg->use_metadata = true; send_cfg->use_metadata = true;
send_cfg->compression_level = 5; send_cfg->compression_level = 5;
send_cfg->chunk_size = 1024; send_cfg->chunk_size = 1024;
send_cfg->ignore_existing = true;
/* Use socketpair for bidirectional communication */ /* Use socketpair for bidirectional communication */
int p[2]; int p[2];
@@ -161,8 +160,6 @@ static void test_config_send_receive() {
ok = false; ok = false;
if (recv_cfg->chunk_size != 1024) if (recv_cfg->chunk_size != 1024)
ok = false; ok = false;
if (!recv_cfg->ignore_existing)
ok = false;
} }
config_delete(recv_cfg); config_delete(recv_cfg);
close(p[0]); close(p[0]);
@@ -188,11 +185,11 @@ static void test_config_send_receive() {
} }
static void test_config_send_receive_version_mismatch() { static void test_config_send_receive_version_mismatch() {
/* A peer using the previous wire format must be rejected. */ /* Create a config with a different protocol version */
Config* cfg = config_create(); Config* cfg = config_create();
EXPECT_NOT_NULL(cfg); EXPECT_NOT_NULL(cfg);
free(cfg->version); free(cfg->version);
cfg->version = str_dup("2.2.0"); cfg->version = str_dup("0.0");
cfg->send_directory = str_dup("/src"); cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst"); cfg->receive_root_directory = str_dup("/dst");
-95
View File
@@ -88,99 +88,6 @@ static void test_file_save_to_disk() {
rmdir("test_save_tmp"); rmdir("test_save_tmp");
} }
static void test_file_save_to_disk_ignore_existing() {
const char* path = "test_ignore_existing_tmp/existing.txt";
EXPECT_TRUE(file_write_to_disk(path, "old", 3, false, false));
File* file = file_create("existing.txt");
EXPECT_NOT_NULL(file);
file->data->data = malloc(3);
EXPECT_NOT_NULL(file->data->data);
memcpy(file->data->data, "new", 3);
file->data->size = 3;
Config* config = config_create();
EXPECT_NOT_NULL(config);
config->ignore_existing = true;
EXPECT_TRUE(file_save_to_disk("test_ignore_existing_tmp", file, config));
FILE* stream = fopen(path, "rb");
char content[4] = {0};
EXPECT_NOT_NULL(stream);
// cppcheck-suppress knownConditionTrueFalse
if (stream) {
EXPECT_EQ_INT((int)fread(content, 1, 3, stream), 3);
fclose(stream);
}
EXPECT_EQ_STR(content, "old");
file_destroy(file);
config_delete(config);
unlink(path);
rmdir("test_ignore_existing_tmp");
}
static void test_file_save_to_disk_ignore_existing_entry_types() {
const char* root = "test_ignore_existing_entries_tmp";
const char* directory = "test_ignore_existing_entries_tmp/directory";
const char* link = "test_ignore_existing_entries_tmp/link";
const char* target = "test_ignore_existing_entries_tmp/target";
const char* backup = "test_ignore_existing_entries_tmp/backup.txt~";
const char* backup_file = "test_ignore_existing_entries_tmp/backup.txt";
Config* config = config_create();
File* file = file_create("unused");
unlink(link);
unlink(target);
unlink(backup);
unlink(backup_file);
rmdir(directory);
rmdir(root);
EXPECT_NOT_NULL(config);
EXPECT_NOT_NULL(file);
// cppcheck-suppress knownConditionTrueFalse
if (!config || !file)
return;
config->ignore_existing = true;
config->backup = true;
file->data->data = malloc(3);
EXPECT_NOT_NULL(file->data->data);
// cppcheck-suppress knownConditionTrueFalse
if (!file->data->data) {
file_destroy(file);
config_delete(config);
return;
}
memcpy(file->data->data, "new", 3);
file->data->size = 3;
EXPECT_EQ_INT(mkdir(root, 0755), 0);
EXPECT_EQ_INT(mkdir(directory, 0755), 0);
EXPECT_TRUE(file_write_to_disk(target, "old", 3, false, false));
EXPECT_EQ_INT(symlink("target", link), 0);
free(file->path);
file->path = str_dup("directory");
EXPECT_TRUE(file_save_to_disk(root, file, config));
free(file->path);
file->path = str_dup("link");
EXPECT_TRUE(file_save_to_disk(root, file, config));
free(file->path);
file->path = str_dup("backup.txt");
EXPECT_TRUE(file_write_to_disk(backup_file, "old", 3, false, false));
EXPECT_TRUE(file_save_to_disk(root, file, config));
EXPECT_TRUE(file_path_exists_secure(backup_file));
EXPECT_FALSE(file_path_exists_secure(backup));
file_destroy(file);
config_delete(config);
unlink(link);
unlink(target);
unlink(backup_file);
rmdir(directory);
rmdir(root);
}
static void test_file_write_to_disk_basic() { static void test_file_write_to_disk_basic() {
const char* content = "Basic file_write_to_disk test"; const char* content = "Basic file_write_to_disk test";
EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content), EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content),
@@ -551,8 +458,6 @@ void test_file() {
test_file_load_data(); test_file_load_data();
test_file_load_data_missing_file(); test_file_load_data_missing_file();
test_file_save_to_disk(); test_file_save_to_disk();
test_file_save_to_disk_ignore_existing();
test_file_save_to_disk_ignore_existing_entry_types();
test_file_write_to_disk_basic(); test_file_write_to_disk_basic();
test_file_write_to_disk_creates_dirs(); test_file_write_to_disk_creates_dirs();
test_file_write_to_disk_does_not_follow_symlink(); test_file_write_to_disk_does_not_follow_symlink();