31 Commits

Author SHA1 Message Date
TapTap c220dc0739 docs: clarify rsync compatibility roadmap
CI / lint (pull_request) Successful in 30s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 34s
2026-08-15 12:49:07 +02:00
TapTap 298976aefb fix: link receiver into fuzz targets
CI / lint (pull_request) Successful in 29s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 16s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 33s
2026-08-15 12:34:19 +02:00
TapTap 3b7f97853c fix: satisfy cppcheck const analysis
CI / lint (pull_request) Successful in 31s
CI / sanitizers (address) (pull_request) Successful in 38s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Failing after 14s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-08-15 12:30:52 +02:00
TapTap 8cca891b9a refactor: split transfer and protocol responsibilities
CI / lint (pull_request) Failing after 30s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-15 12:27:18 +02:00
TapTap 786e686563 fix: satisfy clang-format in CLI test
CI / lint (pull_request) Successful in 33s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 33s
2026-08-15 11:52:05 +02:00
TapTap 046c8d1035 Merge pull request 'Evaluation of codebase by human' (#210) from evaluation-of-codebase-by-human into dev
CI / lint (push) Failing after 3s
CI / build-and-test (push) Has been skipped
CI / sanitizers (address) (push) Has been skipped
CI / sanitizers (undefined) (push) Has been skipped
CI / fuzz-build (push) Has been skipped
CI / coverage (push) Has been skipped
CI / valgrind (push) Has been skipped
2026-08-11 21:24:37 +02:00
TapTap 634cddee3b Merge dev into human codebase evaluation
CI / lint (pull_request) Failing after 2s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-11 21:24:17 +02:00
TapTap 75040103d3 Merge pull request 'Remove unimplemented CLI options' (#209) from feat/remove-unimplemented-cli-options into evaluation-of-codebase-by-human
CI / lint (pull_request) Successful in 25s
CI / sanitizers (address) (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 35s
CI / fuzz-build (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
Reviewed-on: #209
2026-08-11 21:17:52 +02:00
TapTap 852e47a143 Satisfy cppcheck CLI and logger checks
CI / lint (pull_request) Successful in 25s
CI / sanitizers (address) (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 35s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m14s
CI / valgrind (pull_request) Successful in 32s
2026-08-11 21:14:56 +02:00
TapTap 679e389ba6 Fix va_list cleanup in logger
CI / lint (pull_request) Failing after 25s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-11 21:12:00 +02:00
TapTap 2e00fc31f2 Format CLI option test table
CI / lint (pull_request) Failing after 25s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-11 21:08:54 +02:00
TapTap 9396de88db Merge pull request 'fix: complete issues #201-#206 remediation' (#208) from feat/triage-complete into dev
CI / lint (push) Successful in 33s
CI / sanitizers (address) (push) Successful in 37s
CI / sanitizers (undefined) (push) Successful in 36s
CI / fuzz-build (push) Successful in 13s
CI / coverage (push) Successful in 31s
CI / build-and-test (push) Successful in 1m15s
CI / valgrind (push) Successful in 32s
Reviewed-on: #208
2026-08-11 21:06:49 +02:00
TapTap d146e1bb7c Remove unimplemented CLI options
CI / lint (pull_request) Failing after 3s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-11 20:59:06 +02:00
TapTap 09fca8d931 start of human refactoring 2026-08-11 20:26:15 +02:00
TapTap b88e17826e fix: satisfy metadata const lint
CI / lint (pull_request) Successful in 33s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 37s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 33s
2026-08-10 19:20:38 +02:00
TapTap 690bf72d7f fix: close remaining PR 208 review findings
CI / lint (pull_request) Failing after 33s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-10 19:10:02 +02:00
TapTap d367706689 fix: satisfy file API const analysis
CI / lint (pull_request) Successful in 31s
CI / sanitizers (address) (pull_request) Successful in 38s
CI / sanitizers (undefined) (pull_request) Successful in 38s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 32s
2026-08-10 17:30:13 +02:00
TapTap 5f0aed72e7 fix: satisfy metadata const analysis
CI / lint (pull_request) Failing after 31s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-10 17:28:49 +02:00
TapTap 44c2a8bb79 fix: satisfy static analysis in review paths
CI / lint (pull_request) Failing after 31s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-10 17:27:33 +02:00
TapTap 7776a63d3d fix: close remaining PR 208 review gaps
CI / lint (pull_request) Failing after 32s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-10 17:26:00 +02:00
TapTap 5bc319f694 fix: remove redundant poll timeout branch
CI / lint (pull_request) Successful in 26s
CI / sanitizers (address) (pull_request) Successful in 36s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-08-09 11:53:54 +02:00
TapTap 1a0a7a19a5 style: format atomic queue cancellation
CI / lint (pull_request) Failing after 27s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-09 11:52:51 +02:00
TapTap d64666d456 fix: close remaining PR 208 security gaps
CI / lint (pull_request) Failing after 3s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-09 11:52:15 +02:00
TapTap cc143316b2 Merge latest dev into triage remediation
CI / lint (pull_request) Successful in 25s
CI / sanitizers (address) (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 33s
2026-08-09 11:30:08 +02:00
TapTap 33e416324f test: annotate intentional static-analysis guards
CI / lint (pull_request) Successful in 2m9s
CI / sanitizers (address) (pull_request) Successful in 35s
CI / sanitizers (undefined) (pull_request) Successful in 35s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 31s
CI / build-and-test (pull_request) Successful in 1m15s
CI / valgrind (pull_request) Successful in 32s
2026-08-08 20:43:53 +02:00
TapTap a6ec201800 fix: satisfy static analysis in regression paths
CI / lint (pull_request) Failing after 2m10s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-08 20:40:13 +02:00
TapTap 9985a8f6a4 style: format config validation
CI / lint (pull_request) Failing after 2m9s
CI / build-and-test (pull_request) Has been skipped
CI / sanitizers (address) (pull_request) Has been skipped
CI / sanitizers (undefined) (pull_request) Has been skipped
CI / fuzz-build (pull_request) Has been skipped
CI / coverage (pull_request) Has been skipped
CI / valgrind (pull_request) Has been skipped
2026-08-08 20:35:46 +02:00
TapTap 4534284fe9 fix: complete triage security and transfer remediation 2026-08-08 20:35:29 +02:00
TapTap 2ce4f9fbf6 style: format protocol deadline calculation 2026-08-08 20:27:44 +02:00
TapTap 2450b90dd0 feat: add checksum-aware incremental sync 2026-08-08 20:27:26 +02:00
TapTap e8e9436879 fix: harden queue and receiver error handling 2026-08-08 20:25:58 +02:00
49 changed files with 2878 additions and 2065 deletions
+8 -1
View File
@@ -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
+7 -4
View File
@@ -58,15 +58,18 @@ endif()
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
file(GLOB SHARED_SRCS "src/shared/*.c") file(GLOB SHARED_SRCS "src/shared/*.c")
set(FILE_STORE_SRCS src/shared/file_store.c)
list(REMOVE_ITEM SHARED_SRCS ${FILE_STORE_SRCS})
file(GLOB SERVER_SRCS "src/server/*.c") file(GLOB SERVER_SRCS "src/server/*.c")
set(SERVER_RECEIVER_SRCS src/server/receiver.c)
file(GLOB CLIENT_SRCS "src/client/*.c") file(GLOB CLIENT_SRCS "src/client/*.c")
# --- Main executables --- # --- Main executables ---
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS}) add_executable(server ${SERVER_SRCS} ${SHARED_SRCS} ${FILE_STORE_SRCS})
target_include_directories(server PRIVATE src/shared src/server src/client) target_include_directories(server PRIVATE src/shared src/server src/client)
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash) target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS}) add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS} ${FILE_STORE_SRCS} ${SERVER_RECEIVER_SRCS})
target_include_directories(client PRIVATE src/shared src/server src/client) target_include_directories(client PRIVATE src/shared src/server src/client)
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash) target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
@@ -79,7 +82,7 @@ set(TEST_INCLUDES tests src/shared src/server src/client)
# Monolithic test binary (backward compatible) # Monolithic test binary (backward compatible)
file(GLOB TEST_SRCS "tests/test_*.c" "tests/runner.c") file(GLOB TEST_SRCS "tests/test_*.c" "tests/runner.c")
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c src/client/client_cli.c) add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} ${FILE_STORE_SRCS} ${SERVER_RECEIVER_SRCS} src/client/scanner.c src/client/client_cli.c src/client/client_validation.c src/client/usage.c)
target_include_directories(tests PRIVATE ${TEST_INCLUDES}) target_include_directories(tests PRIVATE ${TEST_INCLUDES})
target_compile_definitions(tests PRIVATE FASTSYNC_TEST_BUILD) target_compile_definitions(tests PRIVATE FASTSYNC_TEST_BUILD)
target_link_libraries(tests PRIVATE ${TEST_LIBS}) target_link_libraries(tests PRIVATE ${TEST_LIBS})
@@ -94,7 +97,7 @@ if(ENABLE_FUZZ)
file(GLOB FUZZ_SRCS "tests/fuzz/*.c") file(GLOB FUZZ_SRCS "tests/fuzz/*.c")
foreach(FUZZ_SRC ${FUZZ_SRCS}) foreach(FUZZ_SRC ${FUZZ_SRCS})
get_filename_component(FUZZ_NAME ${FUZZ_SRC} NAME_WE) get_filename_component(FUZZ_NAME ${FUZZ_SRC} NAME_WE)
add_executable(${FUZZ_NAME} ${FUZZ_SRC} ${SHARED_SRCS}) add_executable(${FUZZ_NAME} ${FUZZ_SRC} ${SHARED_SRCS} ${FILE_STORE_SRCS} ${SERVER_RECEIVER_SRCS})
target_include_directories(${FUZZ_NAME} PRIVATE ${TEST_INCLUDES}) target_include_directories(${FUZZ_NAME} PRIVATE ${TEST_INCLUDES})
target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer) target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined) target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined)
+311 -303
View File
@@ -1,369 +1,377 @@
# FastSync # FastSync
A high-performance file synchronization system with SSH and TCP transport, TLS encryption, streaming zstd compression, multithreaded transfer, incremental sync, metadata preservation, and rsync-compatible CLI flags. FastSync is a high-performance file synchronization tool designed to become a
drop-in replacement for common `rsync` workflows. It keeps the familiar
source/destination model and rsync-style options while adding optional
multithreading, streaming zstd compression, chunking, zero-copy TCP transfers,
and native TCP/TLS transports.
## Technical Overview The compatibility target is straightforward:
1. **Dual transport**: custom TCP client-server or SSH subprocess (rsync-style `user@host:/path`) - Existing rsync commands should keep the same meaning.
2. **TLS encryption**: OpenSSL-based TLS 1.2+ for encrypted TCP connections with optional CA verification - FastSync-only performance options should be additive and optional.
3. **Chunked file transfer**: files grouped into configurable-size chunks (default ~10 MB) - A normal compatibility-mode transfer should prioritize rsync filesystem
4. **Streaming zstd compression** (levels 122) using `ZSTD_compressStream2` semantics over maximum throughput.
5. **Multithreading**: producer-consumer pipeline with thread-safe queues (scanner → loader → sender)
6. **Incremental sync**: skip files unchanged since last transfer (compares size + mtime)
7. **Batch incremental**: send incremental checks in batched groups for reduced round-trips
8. **Metadata preservation**: `mode`, `uid`, `gid`, `mtime` restored on disk when enabled
9. **`sendfile()` zero-copy** on TCP (~2× faster on loopback)
10. **SSH ControlMaster** for connection reuse across repeated invocations
11. **Bandwidth limiting**: token-bucket throttling (`--bwlimit`)
12. **`--delete`**: receiver removes files not present in sender manifest
13. **`--exclude` / `--include`**: glob-pattern filename filtering
14. **Path traversal protection**: `..` sequences in file paths are rejected automatically
15. **Connection limits**: server enforces maximum concurrent connections (default 100)
16. **Keep-alive**: periodic `STATUS_KEEPALIVE` messages detect stalled connections
17. **Abort handling**: `SIGINT` sends `STATUS_ABORT` for clean server-side teardown
18. **Atomic writes**: received files are written to a temporary name then atomically renamed
19. **Backup mode**: `--backup` preserves overwritten files with optional `--backup-dir`
20. **Log file**: `--log-file` redirects log output to a file instead of stderr
21. **Transfer statistics**: `--stats` prints summary of transferred bytes, files, and timing
## System Architecture FastSync currently speaks its own protocol to `fastsync-server`. SSH mode
starts that server remotely; it does not yet interoperate with an unmodified
rsync client or rsync daemon. See [Compatibility Status](#compatibility-status)
for the current boundary.
### Client ## Why FastSync
- Recursively scans source directories (BFS), supports exclude and include patterns
- Groups files into chunks (configurable size)
- Streaming zstd compression with configurable level
- Chunk serialization (compact binary format) or per-file transfer
- Incremental transfer: sends file metadata to server, skips unchanged files
- Batch incremental: groups incremental checks to minimize round-trips
- Manifests all sent paths when `--delete` is active
- Sends via TCP `sendfile()` or SSH pipe
- Optional progress display with throughput
- Bandwidth limiting via token-bucket algorithm
- Configurable I/O and connection timeouts (`--timeout`, `--contimeout`)
- Quiet mode (`-q`/`--quiet`) suppresses all non-error output
- Backup overwritten files (`--backup`) with optional directory (`--backup-dir`)
- Transfer statistics summary (`--stats`)
- Maximum directory depth control (`--max-depth`)
- Log file output (`--log-file`)
- Configurable multithreaded queue size (`--queue-size`)
- Exclude patterns from file (`--exclude-from`)
### Server FastSync uses a producer-consumer transfer pipeline and can combine several
- TCP mode: listens on configurable port (default 8080); SSH mode: runs via `--stdio` optimizations for large or high-latency transfers:
- TLS mode: wraps TCP connections with OpenSSL with optional CA verification
- Receives and reassembles files
- Decompresses (streaming zstd), deserializes, restores metadata
- Handles incremental checks: compares size + mtime against destination files
- Handles batch incremental checks for reduced round-trips
- Processes `STATUS_MANIFEST` for `--delete`: walks destination tree, removes extras
- Per-connection concurrency via `fork()` with configurable connection limit (default 100)
- Thread pool for parallel processing
- Atomic writes: files written to `.tmp` path then atomically renamed on success
- Abort handling: cleanly shuts down on `STATUS_ABORT` from client
- Path traversal protection: rejects file paths containing `..`
## Protocol Details - Multithreaded scanning, loading, and sending.
- Streaming zstd compression with levels 1 through 22.
- Configurable file chunking and compact chunk serialization.
- `sendfile()` zero-copy transfers over TCP.
- Batched incremental checks to reduce round trips.
- Optional block-level delta transfer for FastSync peers.
- Bandwidth limiting, progress reporting, statistics, and backups.
- TCP, SSH, and TLS transports.
- Atomic temporary-file writes by default.
### Status Codes These optimizations are disabled or selected independently. Users can start
| Code | Meaning | with rsync-style commands and add FastSync options when they are useful.
|------|---------|
| `STATUS_OK` | Operation successful |
| `STATUS_ERROR` | Error occurred |
| `STATUS_FINISHED` | Transfer complete |
| `STATUS_NEXT` | Ready for next file (per-file mode) |
| `STATUS_CHUNK` | Following data is a serialized chunk |
| `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_BATCH` | Batch incremental check: multiple file checks sent in one message |
| `STATUS_KEEPALIVE` | Keep-alive heartbeat to detect stalled connections |
| `STATUS_ABORT` | Abort signal: client interrupts, server cleans up and exits |
| `STATUS_DELTA_SIGNATURE` | Delta sync: following data is a file signature (rsync-style rolling hash) |
| `STATUS_DELTA_DATA` | Delta sync: following data is a delta patch for a file |
### Wire Format — Metadata ## Compatibility Status
When `use_metadata` is enabled (`-M`), each file entry carries a 4-byte `present` flag followed by five fields (`mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec`). When disabled globally, no metadata bytes are sent — zero wire overhead. FastSync is currently an rsync-compatible CLI in progress, not a complete
replacement for every rsync feature or protocol mode.
### Transfer Flow ### Working today
```
Config → (STATUS_NEXT | STATUS_CHUNK | STATUS_CHECK | STATUS_CHECK_BATCH)* → [STATUS_MANIFEST] → STATUS_FINISHED → STATUS_OK
```
Keep-alive (`STATUS_KEEPALIVE`) may be sent at any point during the transfer. The receiver resets its inactivity timer on receipt. If no data arrives within the receive timeout, the connection is aborted. - Recursive directory scanning.
- Rsync-style source and destination arguments.
- SSH transport using `user@host:/path` destinations.
- TCP client/server transfers.
- Dry runs, excludes, includes, size filters, backups, statistics, and
bandwidth limiting.
- Incremental size/mtime checks and optional xxHash64 content checks.
- FastSync-native delta transfer for changed files.
- Optional mode and timestamp preservation.
- Delete manifests with server-side delete authorization.
- Temporary-file writes with atomic rename by default.
- Path traversal checks and destination-root confinement.
Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up temporary files and exits the child process. ### Not yet equivalent to rsync
### Protocol Version - The FastSync wire protocol is not the rsync wire protocol.
- SSH mode requires `fastsync-server` on the remote host.
- Archive mode does not yet provide all of rsync's `-rlptgoD` behavior.
- Symlink transfer is incomplete; link targets are not yet recreated in all
modes.
- Owner/group, ACL, xattr, hard-link, device, and special-file handling is
incomplete or unavailable.
- Sparse-file handling does not yet preserve all holes correctly.
- `--partial`, `--partial-dir`, `--append`, and `--append-verify` are not yet
full rsync-style resumable transfers.
- Several rsync short options currently have FastSync-specific meanings. Do
not assume every short option is interchangeable yet.
`1.3.0` — server and client must match. Mismatch results in `STATUS_ERROR`. The detailed flag matrix is maintained in
[`RSYNC_COMPAT.md`](RSYNC_COMPAT.md). It distinguishes implemented,
partial, alternate, and planned behavior.
## Command-Line Arguments ## Quick Start
### Client ### Build
| Argument | Description | Requirements: C11 compiler, CMake 3.22 or newer, zstd, OpenSSL, pthreads,
|----------|-------------| and an SSH client for SSH transport.
| Positional | `<source> <dest>` — automatic SSH detection if dest contains `:` |
| `-c [level]` | Compression with optional level (122, default 5) |
| `-z [level]` | Alias for `-c` |
| `-a, --archive` | Archive mode: enables `-c -m -M` (no `-s`) |
| `-m` | Multithreading mode |
| `-s` | Chunk serialization (batch all files per chunk) |
| `-f, --sendfile` | Sendfile zero-copy. Incompatible with `-c` / `-s`. TCP only. |
| `-M, --preserve` | Preserve file metadata (mode, uid, gid, mtime) |
| `-n, --dry-run` | Scan and print what would be transferred |
| `-p <port>` | SSH port (default: 22) |
| `-v, --verbose` | Enable debug logging |
| `-q, --quiet` | Suppress all non-error output |
| `--silent` | Alias for `--quiet` |
| `--progress` | Show real-time transfer speed |
| `--delete` | Delete files on receiver not present in source |
| `--exclude <pattern>` | Exclude files matching glob pattern (repeatable) |
| `--exclude-from <file>` | Read exclude patterns from a file (one per line) |
| `--include <pattern>` | Only transfer files matching glob pattern (repeatable, whitelist) |
| `--max-size <n>` | Skip files larger than n bytes |
| `--min-size <n>` | Skip files smaller than n bytes |
| `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `-s`. |
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
| `--timeout <sec>` | I/O timeout in seconds (default: 30) |
| `--contimeout <sec>` | Connection timeout in seconds (default: 10) |
| `--backup` | Backup existing destination files before overwriting |
| `--backup-dir <dir>` | Target directory for backups (requires `--backup`) |
| `--stats` | Print transfer statistics at end (bytes, files, timing) |
| `--max-depth <n>` | Maximum directory depth to recurse (0 = unlimited, default: 0) |
| `--log-file <path>` | Write log messages to file instead of stderr |
| `--queue-size <n>` | Queue capacity for multithreaded mode (default: 100) |
| `--source-dir <path>` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) |
| `--dest-dir <path>` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) |
| `--save-to-disk` | Write received files to disk |
| `--server-host <ip>` | Server IP address (default: `127.0.0.1`) |
| `--server-port <n>` | Server port (default: `8080`) |
| `--tls` | Enable TLS encryption |
| `--cert <path>` | TLS certificate file (PEM) |
| `--key <path>` | TLS private key file (PEM) |
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
### Server
| Argument | Description |
|----------|-------------|
| `--stdio` | Run in stdio mode (for SSH transport; single connection then exits) |
| `-p <port>` | TCP listen port (default: 8080, range: 165535) |
| `--tls` | Enable TLS encryption |
| `--cert <path>` | TLS certificate file (PEM) |
| `--key <path>` | TLS private key file (PEM) |
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
| `-v, --verbose` | Enable debug logging |
| `--help` | Show help |
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `FASTSYNC_SOURCE_DIR` | — | Source directory fallback |
| `FASTSYNC_DEST_DIR` | — | Destination directory fallback |
| `FASTSYNC_SAVE_TO_DISK` | `false` | Disk persistence fallback |
| `FASTSYNC_SSH_PORT` | `22` | Default SSH port |
| `FASTSYNC_SERVER_HOST` | `127.0.0.1` | Default server host |
| `FASTSYNC_SERVER_PORT` | `8080` | Default server port |
| `FASTSYNC_TLS_CERT` | — | Default TLS certificate path |
| `FASTSYNC_TLS_KEY` | — | Default TLS private key path |
| `FASTSYNC_TLS_CA` | — | Default TLS CA certificate path |
## Implementation Details
### Data Structures
1. **Chunk** — collection of files (~10 MB total by default)
2. **File** — path, content (`Data`), optional `FileMetadata` pointer
3. **FileMetadata**`mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec`
4. **Config** — runtime parameters (transported over wire, TLS settings excluded). Includes `timeout`, `contimeout`, `quiet`, `backup`, `backup_dir`, `stats`, `max_depth`, `log_file`, `queue_size`.
5. **Queue** — thread-safe bounded queue with condition variables
6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support, max-depth enforcement
### Key Algorithms
1. **File scanning** — BFS directory traversal; entries matched against exclude and include patterns, max-depth enforced
2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed
3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream`
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.
6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks
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
9. **SSH transport**`socketpair()` + `fork()` + `execvp("ssh", ...)` with `ControlMaster` and port support
10. **TLS transport** — OpenSSL `SSL_CTX` with TLS 1.2 minimum, optional CA verification, transparent `SSL_read`/`SSL_write` via `io_set_ssl()`
11. **Path traversal protection**`has_path_traversal()` rejects any file path containing `..` components, preventing directory escape attacks
12. **Connection limiting** — server tracks active connections and rejects new ones beyond `max_connections` (default 100)
13. **Keep-alive** — idle connections receive periodic `STATUS_KEEPALIVE` to detect half-open TCP connections
14. **Abort handling**`SIGINT` sets an abort flag; the next protocol operation sends `STATUS_ABORT` for clean server cleanup
15. **Atomic writes** — files are written to a `.tmp` suffix then atomically renamed via `rename()`, preventing partial files
16. **Backup** — before overwriting, existing files are moved to `--backup-dir` (or same directory with `~` suffix) preserving the original
## Security Features
### Path Traversal Protection
All received file paths are validated by `has_path_traversal()` before any disk operation. Any path containing `..` components is rejected with `STATUS_ERROR`, preventing directory escape attacks.
### TLS Certificate Verification
When `--ca` is provided, the server performs mutual TLS verification (`SSL_VERIFY_PEER` with depth 4). Without `--ca`, TLS is still encrypted but peer certificates are not verified.
### Connection Limits
The server enforces a maximum of 100 concurrent connections (configurable via `max_connections` in `Server`). When the limit is reached, new connections are immediately rejected and closed.
### Abort Handling
If the client receives `SIGINT` (Ctrl+C) during a transfer, it sends `STATUS_ABORT` to the server. The server then cleans up temporary files and exits the child process, preventing incomplete files from remaining on disk.
### Atomic Writes
Received files are written to a temporary path (suffixed with `.tmp`) and then atomically renamed to the final filename via `rename()`. This prevents partial or corrupted files from appearing at the destination if the transfer is interrupted.
## Build Requirements
- C11 compiler
- CMake >= 3.22
- zstd library
- OpenSSL (development headers and libraries)
- pthreads
- SSH client (for SSH transport mode only)
### Installing Dependencies
**Ubuntu/Debian:**
```bash
sudo apt install cmake build-essential libzstd-dev libssl-dev openssh-client
```
**Nix:**
```bash
nix-shell # provides zstd, openssl, cmake, gcc
```
## Building
```bash ```bash
cmake -B build -S . && cmake --build build -j$(nproc) cmake -B build -S .
cmake --build build -j$(nproc)
``` ```
## Running With Nix:
### Server (TCP mode)
```bash ```bash
./build/server nix-shell
cmake -B build -S .
cmake --build build -j$(nproc)
``` ```
### Server with TLS ### SSH transfer
The remote host must have `fastsync-server` available in `PATH`, or use
`--fastsync-server-path`.
```bash ```bash
./build/server --tls --cert server.pem --key server-key.pem ./build/client /path/to/source user@host:/path/to/destination
``` ```
### Server via SSH ### TCP transfer
Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh user@host fastsync-server --stdio` automatically when an SSH-style destination is given.
Start the FastSync server:
### Client — SSH (rsync-style)
```bash ```bash
./build/client /path/to/send user@host:/path/to/receive ./build/server -p 8080
``` ```
### Client — TCP Then run the client:
```bash ```bash
./build/client --source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk ./build/client --server-host 127.0.0.1 --server-port 8080 \
--source-dir /path/to/source --dest-dir /path/to/destination \
--save-to-disk
``` ```
### Client — TCP with TLS ### TLS transfer
```bash ```bash
./build/server --tls --cert server.pem --key server-key.pem -p 8443
./build/client --tls --cert client.pem --key client-key.pem --ca ca.pem \ ./build/client --tls --cert client.pem --key client-key.pem --ca ca.pem \
--source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk --server-host example.com --server-port 8443 \
--source-dir /path/to/source --dest-dir /path/to/destination \
--save-to-disk
``` ```
### Common Options ## Common Workflows
These examples show the intended rsync-style workflow. Options marked as
FastSync-native are optional performance or transport extensions.
```bash ```bash
# Archive mode (compression + multithreading + metadata) # Basic synchronization
./build/client -a /path/to/send user@host:/path ./build/client /source/ /destination/
# Dry run # Archive-style synchronization (current FastSync archive behavior)
./build/client -n /path/to/send /path/to/receive ./build/client -a /source/ user@host:/destination/
# With progress and custom chunk size # Preview a transfer without changing the destination
./build/client --progress --chunk-size 2097152 /src user@host:/dst ./build/client -n /source/ /destination/
# Exclude temporary files + delete extras on receiver # Exclude temporary and object files
./build/client --exclude "*.tmp" --exclude "*.o" --delete /src user@host:/dst ./build/client --exclude '*.tmp' --exclude '*.o' \
/source/ user@host:/destination/
# Incremental sync (skip unchanged files) # Remove destination entries not present in the source
./build/client --incremental /src user@host:/dst ./build/client --delete /source/ user@host:/destination/
# Bandwidth limit to 1 MB/s # Skip unchanged files using size and modification time
./build/client --bwlimit 1024 /src user@host:/dst ./build/client --incremental /source/ user@host:/destination/
# With timeouts, quiet mode, and stats # Verify content when size and time are not sufficient
./build/client --timeout 60 --contimeout 15 --quiet --stats /src user@host:/dst ./build/client --incremental --checksum /source/ user@host:/destination/
# Backup overwritten files to a directory # Preserve supported mode and timestamp metadata
./build/client --backup --backup-dir /backups /src user@host:/dst ./build/client -M /source/ user@host:/destination/
# Exclude patterns from file, limit depth # Keep backups of overwritten destination files
./build/client --exclude-from ignore.txt --max-depth 3 /src user@host:/dst ./build/client --backup --backup-dir /backups \
/source/ user@host:/destination/
# Custom queue size for multithreading
./build/client -m --queue-size 200 /src user@host:/dst
# Log to file
./build/client --log-file /tmp/fastsync.log /src user@host:/dst
# All features
./build/client -a --progress --chunk-size 5242880 --exclude "*.log" --delete /src /dst
``` ```
## FastSync Extensions
FastSync-native options are intended to add performance or operational
features without changing the meaning of ordinary compatibility options.
| Option | Purpose |
|---|---|
| `-m` | Enable the multithreaded scanner/loader/sender pipeline. |
| `-c [level]`, `-z [level]` | Enable streaming zstd compression, levels 1-22. |
| `--compress-level <n>` | Set the zstd compression level. |
| `--chunk-size <bytes>` | Set the transfer chunk size. |
| `-s` | Enable FastSync chunk serialization. |
| `-f`, `--sendfile` | Use TCP `sendfile()` zero-copy transfer. Incompatible with compression and chunk serialization. |
| `--delta` | Use FastSync-native block delta transfer. Requires `--incremental`. |
| `--delta-block <bytes>` | Set the FastSync delta block size. |
| `--delta-max <bytes>` | Limit files eligible for FastSync delta transfer. |
| `--server-host <host>` | Select the TCP server host. |
| `--server-port <port>` | Select the TCP server port. |
| `--tls` | Enable TLS for TCP transport. |
| `--bwlimit <KB/s>` | Apply token-bucket bandwidth limiting. |
| `--progress` | Show transfer progress and throughput. |
| `--stats` | Print transfer statistics. |
| `--timeout <seconds>` | Set I/O timeout. |
| `--contimeout <seconds>` | Set connection timeout. |
Current short-option conflicts are tracked as compatibility work. In
particular, FastSync currently uses `-p` for SSH port, `-s` for chunk
serialization, and `-S` for sparse handling. These meanings must be reconciled
before FastSync can claim full rsync CLI compatibility.
## Client Options
### Selection and transfer
| Option | Description |
|---|---|
| `-a`, `--archive` | Enable current archive preset. Full rsync archive semantics are planned. |
| `-n`, `--dry-run` | Scan and report without writing files. |
| `--delete` | Request removal of destination entries absent from the source. The server must allow deletion. |
| `--exclude <pattern>` | Exclude matching paths. Repeatable. |
| `--include <pattern>` | Include matching paths. Repeatable. |
| `--exclude-from <file>` | Read exclude patterns from a file. |
| `--include-from <file>` | Read include patterns from a file. |
| `--max-size <bytes>` | Skip files larger than the limit. |
| `--min-size <bytes>` | Skip files smaller than the limit. |
| `--max-depth <n>` | Limit recursive scanning depth; zero means unlimited. |
| `--incremental` | Skip files matching destination size and mtime. |
| `--checksum` | Include xxHash64 content checks in incremental comparisons. |
| `--backup` | Back up overwritten files. |
| `--backup-dir <dir>` | Store backups under a separate directory. |
| `--suffix <suffix>` | Set the backup filename suffix. |
| `--partial` | Keep received data under the configured partial location. Full resume semantics are planned. |
| `--partial-dir <dir>` | Set the partial transfer directory. |
| `--inplace` | Write directly to the destination instead of using a temporary file. |
### Metadata and links
| Option | Description |
|---|---|
| `-M`, `--preserve` | Preserve supported file metadata, currently mode and modification time. |
| `-l`, `--links` | Request symlink preservation; link-target transfer remains incomplete. |
| `--copy-links` | Copy symlink referents. |
| `--safe-links` | Skip symlinks that point outside the transfer tree. |
| `--copy-unsafe-links` | Copy unsafe symlink referents. |
| `-S`, `--sparse` | Request sparse-file handling; full hole preservation is planned. |
### Output and logging
| Option | Description |
|---|---|
| `-v`, `--verbose` | Enable debug logging. |
| `--progress` | Show live transfer progress. |
| `--stats` | Print transfer statistics. |
| `--log-file <path>` | Write log output to a file. |
| `-V`, `--version` | Print the FastSync protocol version. |
| `--help` | Print command usage. |
### Paths and transport
| Option | Description |
|---|---|
| `-p <port>` | SSH port in the current CLI. This conflicts with rsync's `-p` permissions option and is planned for correction. |
| `--fastsync-server-path <path>` | Remote FastSync server path for SSH mode. |
| `--source-dir <path>` | Set the source directory explicitly. |
| `--dest-dir <path>` | Set the destination directory explicitly. |
| `--save-to-disk` | Enable server-side disk persistence. |
| `--server-host <host>` | TCP server address. |
| `--server-port <port>` | TCP server port. |
| `--tls` | Enable TLS. Requires `--cert` and `--key`. |
| `--cert <path>` | TLS certificate file. |
| `--key <path>` | TLS private key file. |
| `--ca <path>` | CA file for peer verification. |
## Server Options
| Option | Description |
|---|---|
| `--stdio` | Serve one SSH connection over standard input/output. |
| `-p <port>` | TCP listen port. |
| `--tls` | Enable TLS. |
| `--cert <path>` | TLS certificate file. |
| `--key <path>` | TLS private key file. |
| `--ca <path>` | CA file for peer verification. |
| `--allow-delete` | Permit client delete manifests. Deletion is refused by default. |
| `-v`, `--verbose` | Enable debug logging. |
| `--help` | Print server usage. |
## Architecture
### Client
- Recursively scans the source tree with include, exclude, size, and depth
filters.
- Sends individual files or serialized chunks.
- Performs incremental checks and optional content checksums.
- Uses a multithreaded producer-consumer pipeline when requested.
- Sends over TCP, TLS-wrapped TCP, or an SSH subprocess.
- Supports progress, statistics, backups, timeouts, and bandwidth limiting.
### Server
- Runs as a TCP listener or one-shot SSH `--stdio` server.
- Receives and reassembles files and decompresses streaming zstd data.
- Applies supported metadata and writes files through a confined destination
root.
- Uses temporary files and atomic rename by default.
- Handles delete manifests only when explicitly authorized.
- Enforces connection, message-size, and path-safety limits.
## Protocol and Security
FastSync protocol version `2.2.0` is shared by the client and server. The
current protocol is sender-driven and includes configuration negotiation,
incremental checks, checksums, manifests, keep-alives, abort handling, and
FastSync-native delta messages. Client and server versions must currently
match exactly.
TLS provides encrypted TCP transport. Supplying `--ca` enables certificate
verification; without it, traffic is encrypted but peer identity is not
verified. Use certificate verification for deployments where authentication
matters. The default TCP transport is not encrypted.
The receiver protects its destination root with path validation, `openat()`
directory traversal, `O_NOFOLLOW`, temporary files, and atomic renames. Delete
operations require the server's explicit `--allow-delete` policy.
## Compatibility Roadmap
The project will reach the drop-in replacement goal in stages:
1. Correct rsync option meanings, including short options, combined options,
and `--option=value` syntax.
2. Add differential tests that compare FastSync and rsync contents, metadata,
links, deletes, filters, dry runs, and exit codes.
3. Make `-a` implement the expected recursive, links, permissions, times,
owner/group, and supported special-file behavior.
4. Complete symlink, sparse-file, metadata, delete-policy, and resumable-write
semantics.
5. Add rsync remote-shell and daemon protocol interoperability.
6. Keep FastSync performance options as negotiated, optional extensions.
The exhaustive implementation matrix and compatibility notes are in
[`RSYNC_COMPAT.md`](RSYNC_COMPAT.md).
## Testing ## Testing
```bash Run the unit test binary:
# Unit tests (18 suites — array_list, chunk, compression, config, data, delta, file, glob,
# metadata, property, protocol, queue, robustness, scanner,
# shared_utils, stress, transport_tcp, transport_ssh, transport_tls)
./build/tests
# Integration + benchmark suite ```bash
python3 test.py ./build/tests
``` ```
The benchmark prints throughput metrics, best configuration, and speedup vs rsync. Run the Python integration suite:
## Performance Considerations ```bash
python3 -m pytest tests/
```
1. Chunk size (~10 MB default) balances memory and transfer efficiency For stricter local validation:
2. Compression level trades CPU for bandwidth
3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files
4. Multithreading scales with core count; `--queue-size` controls pipeline buffering
5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled)
6. SSH socketpair buffer set to 1 MB for improved pipe throughput
7. SSH ControlMaster reuses connections across repeated invocations
8. Incremental sync eliminates redundant transfers entirely
9. Batch incremental reduces round-trips by grouping multiple checks into one message
10. Bandwidth limiting uses token-bucket with nanosleep for accurate throttling
11. Atomic writes add a single `rename()` per file — negligible overhead
12. Path traversal check is O(n) in path length with negligible cost
## Benchmark Results ```bash
cmake -B build-strict -S . -DSTRICT_WARNINGS=ON
cmake --build build-strict -j$(nproc)
cmake -B build-asan -S . -DSANITIZER=address
cmake --build build-asan -j$(nproc)
```
25 MB of mixed file sizes over `localhost` with disk I/O throttled (reads ≤ 15 MB/s, writes ≤ 10 MB/s) and network emulation via `tc netem`. Each test was run 3×; the median is reported below. The benchmark tool compares FastSync configurations with rsync under
controlled local and network conditions:
### LAN (1000 Mbit, 20 ms ±1 ms, 0.1% loss) ```bash
python3 benchmark/bench.py --help
```
| Configuration | Time | vs rsync (archive) | vs rsync (compress) | Benchmark results measure transfer performance only. They do not establish
|---|---|---|---| rsync protocol or filesystem-semantic compatibility.
| **Best: `-m -c`** | **0.20 s** | **11.2× faster** | **3.6× faster** |
| Compression (`-c`) | 0.31 s | 7.3× faster | 2.3× faster |
| Standard | 1.27 s | 1.8× faster | — |
| rsync (archive) | 2.27 s | — | — |
| rsync (archive + compress) | 0.72 s | — | — |
### WAN (100 Mbit, 50 ms ±10 ms, 1% loss) ## Performance Guidance
| Configuration | Time | vs rsync (archive) | vs rsync (compress) | - Use `-m` for workloads with many files or enough CPU parallelism.
|---|---|---|---| - Use `-c` or `-z` when network bandwidth is more constrained than CPU.
| **Best: `-m -c`** | **0.39 s** | **44.8× faster** | **3.8× faster** | - Tune `--chunk-size` for file sizes, memory limits, and network latency.
| Compression (`-c`) | 0.64 s | 27.3× faster | 2.3× faster | - Use `-f` for large uncompressed TCP transfers where zero-copy I/O helps.
| Standard | 7.12 s | 2.4× faster | — | - Use `--incremental` to avoid retransmitting unchanged files.
| rsync (archive) | 17.44 s | — | — | - Use `--delta` for changed files when both endpoints are FastSync peers.
| rsync (archive + compress) | 1.47 s | — | — | - Use `--bwlimit` when sharing a link with other traffic.
Compression reduces the data on the wire enough that the transfer becomes latency-bound rather than bandwidth-bound. On WAN, the best configuration runs 10.8× faster than the theoretical limit for uncompressed data, since zstd shrinks the 25 MB payload to a fraction of its original size over the wire. Always validate the compatibility behavior required by a deployment before
replacing an existing rsync job.
+42 -43
View File
@@ -6,10 +6,10 @@ This document maps rsync's full feature set to FastSync's current implementation
| Status | Count | Description | | Status | Count | Description |
|--------|-------|-------------| |--------|-------|-------------|
| ✅ Implemented | 39 | Feature works end-to-end | | ✅ Implemented | 34 | Feature works end-to-end |
| 🔀 Alt Arg | 5 | Functionality exists but under different flag/semantics | | 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
| ⚠️ Partial | 30 | Flag parsed/stored but behavior incomplete | | ⚠️ Partial | 1 | Flag parsed/stored but behavior incomplete |
| ❌ Not Implemented | 62 | Flag not recognized or no behavior | | ❌ Not Implemented | 98 | Flag not recognized or no behavior |
| **Total** | **136** | | | **Total** | **136** | |
--- ---
@@ -20,31 +20,31 @@ This document maps rsync's full feature set to FastSync's current implementation
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) | | `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) |
| `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` | | `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` |
| `-q`, `--quiet` | Suppress non-error messages | Implemented | `quiet` config field | | `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect |
| `-h`, `--help` | Show help | ✅ Implemented | Prints usage and exits | | `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted |
| `-V`, `--version` | Print version | ❌ Not Implemented | | | `-V`, `--version` | Print version | Implemented | |
| `--info=FLAGS` | Fine-grained info verbosity | ⚠️ Partial | `info_level` stored, not wired | | `--info=FLAGS` | Fine-grained info verbosity | ❌ Not Implemented | Removed because it had no effect |
| `--debug=FLAGS` | Fine-grained debug verbosity | ⚠️ Partial | `debug_level` stored, not wired | | `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect |
| `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | | | `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | |
| `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | | | `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | |
| `--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 | ⚠️ Partial | `cvs_exclude` stored, not wired | | `-C`, `--cvs-exclude` | Auto-ignore CVS files | ❌ Not Implemented | Removed because it had no effect |
## 2. Modifying Output ## 2. Modifying Output
| Flag | Rsync Description | FastSync Status | Notes | | Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--stats` | Give transfer stats | ✅ Implemented | Prints file/byte counts | | `--stats` | Give transfer stats | ✅ Implemented | Prints file/byte counts |
| `-h`, `--human-readable` | Human-readable numbers | ⚠️ Partial | `human_readable` stored, not wired | | `-h`, `--human-readable` | Human-readable numbers | ❌ Not Implemented | Removed because it had no effect |
| `-i`, `--itemize-changes` | Per-file change summary | ⚠️ Partial | `itemize_changes` stored, not wired | | `-i`, `--itemize-changes` | Per-file change summary | ❌ Not Implemented | Removed because it had no effect |
| `--progress` | Show progress | ✅ Implemented | Progress callback in sender | | `--progress` | Show progress | ✅ Implemented | Progress callback in sender |
| `-P` | Same as --partial --progress | ❌ Not Implemented | | | `-P` | Same as --partial --progress | ❌ Not Implemented | |
| `--out-format=FORMAT` | Custom output format | ⚠️ Partial | `out_format` stored, not wired | | `--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-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | |
| `--list-only` | List files instead of copying | ⚠️ Partial | `list_only` stored, not wired | | `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect |
## 3. File Selection ## 3. File Selection
@@ -52,8 +52,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 | ⚠️ Partial | `filters` ArrayList stored, not wired | | `--filter=RULE` | Add file-filtering rule | ❌ Not Implemented | Removed because it had no effect |
| `--files-from=FILE` | Read source file list from file | ⚠️ Partial | `files_from` stored, not wired | | `--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 |
| `--min-size=SIZE` | Skip files smaller than SIZE | ✅ Implemented | `min_size` in scanner | | `--min-size=SIZE` | Skip files smaller than SIZE | ✅ Implemented | `min_size` in scanner |
@@ -69,7 +69,7 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior | | `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior |
| `-R`, `--relative` | Use relative path names | ⚠️ Partial | `relative` stored, not wired | | `-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 | |
| `--mkpath` | Create missing path components | ❌ Not Implemented | | | `--mkpath` | Create missing path components | ❌ Not Implemented | |
@@ -78,10 +78,10 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-u`, `--update` | Skip files newer on receiver | ⚠️ Partial | `update` stored, not wired | | `-u`, `--update` | Skip files newer on receiver | ❌ Not Implemented | Removed because it had no effect |
| `--inplace` | Update files in-place | ✅ Implemented | Direct write mode | | `--inplace` | Update files in-place | ✅ Implemented | Direct write mode |
| `--append` | Append data to shorter files | ⚠️ Partial | `append` stored, not wired | | `--append` | Append data to shorter files | ❌ Not Implemented | Removed because it had no effect |
| `--append-verify` | Append with old-data checksum | ⚠️ Partial | `append_verify` stored, not wired | | `--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 | | `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | Parsed as `--delta-block`; controls delta transfer block size |
@@ -100,15 +100,15 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field | | `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field |
| `--delete-before` | Delete before transfer | ⚠️ Partial | `delete_before` stored, not wired | | `--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 | |
| `--delete-after` | Delete after transfer | ⚠️ Partial | `delete_after` stored, not wired | | `--delete-after` | Delete after transfer | ❌ Not Implemented | Removed because it had no effect |
| `--delete-excluded` | Also delete excluded files | ⚠️ Partial | `delete_excluded` stored, not wired | | `--delete-excluded` | Also delete excluded files | ❌ Not Implemented | Removed because it had no effect |
| `--max-delete=NUM` | Max files to delete | ⚠️ Partial | `max_delete` stored, not wired | | `--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 | ⚠️ Partial | `prune_empty_dirs` stored, not wired | | `--prune-empty-dirs` | Prune empty dir chains | ❌ Not Implemented | Removed because it had no effect |
## 8. Metadata Preservation ## 8. Metadata Preservation
@@ -121,11 +121,11 @@ This document maps rsync's full feature set to FastSync's current implementation
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M | | `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
| `-E`, `--executability` | Preserve executability | ❌ Not Implemented | | | `-E`, `--executability` | Preserve executability | ❌ Not Implemented | |
| `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | | | `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | |
| `-A`, `--acls` | Preserve ACLs | ⚠️ Partial | `preserve_acls` stored, not wired | | `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect |
| `-X`, `--xattrs` | Preserve extended attributes | ⚠️ Partial | `preserve_xattrs` stored, not wired | | `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect |
| `-H`, `--hard-links` | Preserve hard links | ⚠️ Partial | `preserve_hard_links` stored, not wired | | `-H`, `--hard-links` | Preserve hard links | ❌ Not Implemented | Removed because it had no effect |
| `-D` | Same as --devices --specials | 🔀 Alt Arg | Maps to --devices only (no --specials) | | `-D` | Same as --devices --specials | ❌ Not Implemented | Removed because device-file handling is not implemented |
| `--devices` | Preserve device files | ⚠️ Partial | `preserve_devices` stored, not wired | | `--devices` | Preserve device files | ❌ Not Implemented | Removed because it had no effect |
| `--specials` | Preserve special files | ❌ Not Implemented | | | `--specials` | Preserve special files | ❌ Not Implemented | |
| `--copy-devices` | Copy device contents as file | ❌ Not Implemented | | | `--copy-devices` | Copy device contents as file | ❌ Not Implemented | |
| `--write-devices` | Write to devices as files | ❌ Not Implemented | | | `--write-devices` | Write to devices as files | ❌ Not Implemented | |
@@ -159,11 +159,11 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-c`, `--checksum` | Skip based on checksum | 🔀 Alt Arg | `-c` means compression; `--checksum` stored and forwarded to scanner | | `--checksum` | Skip based on checksum | ❌ Not Implemented | Removed because it had no effect; `-c` means compression |
| `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally | | `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally |
| `--compare-dest=DIR` | Compare dest files relative to DIR | ⚠️ Partial | `compare_dest` stored, not wired | | `--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 | ⚠️ Partial | `copy_dest` stored, not wired | | `--copy-dest=DIR` | Include copies of unchanged files | ❌ Not Implemented | Removed because it had no effect |
| `--link-dest=DIR` | Hardlink to files when unchanged | ⚠️ Partial | `link_dest` stored, not wired | | `--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 | | | `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | |
## 12. Compression ## 12. Compression
@@ -171,7 +171,7 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-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 | ⚠️ Partial | `compress_choice` stored, always zstd | | `--compress-choice=STR` | 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` | 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 |
@@ -180,22 +180,22 @@ 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 |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `-e`, `--rsh=COMMAND` | Remote shell to use | Implemented | SSH transport support | | `-e`, `--rsh=COMMAND` | Remote shell to use | ❌ Not Implemented | Removed; SSH invokes `ssh` directly |
| `--rsync-path=PROGRAM` | rsync binary on remote | Implemented | `rsync_path` config field | | `--rsync-path=PROGRAM` | rsync binary on remote | ❌ Not Implemented | Removed; use `--fastsync-server-path` |
| `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field | | `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field |
| `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | | | `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | |
| `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | | | `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | |
| `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | | | `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | |
| `--address=ADDRESS` | Bind address for outgoing socket | Implemented | `address` config field | | `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect |
| `-4`, `--ipv4` | Prefer IPv4 | Implemented | `ipv4` config field | | `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect |
| `-6`, `--ipv6` | Prefer IPv6 | Implemented | `ipv6` config field | | `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect |
## 14. Daemon Mode ## 14. Daemon Mode
| Flag | Rsync Description | FastSync Status | Notes | | Flag | Rsync Description | FastSync Status | Notes |
|------|-------------------|-----------------|-------| |------|-------------------|-----------------|-------|
| `--daemon` | Run as rsync daemon | ⚠️ Partial | `daemon` stored, not wired | | `--daemon` | Run as rsync daemon | ❌ Not Implemented | Removed because it had no effect |
| `--config=FILE` | Alternate rsyncd.conf file | ⚠️ Partial | `daemon_config` stored, not wired | | `--config=FILE` | Alternate rsyncd.conf file | ❌ Not Implemented | Removed because it had no effect |
| `--dparam=OVERRIDE` | Override global daemon config | ❌ Not Implemented | | | `--dparam=OVERRIDE` | Override global daemon config | ❌ Not Implemented | |
| `--no-detach` | Don't detach from parent | ❌ Not Implemented | | | `--no-detach` | Don't detach from parent | ❌ Not Implemented | |
| `--password-file=FILE` | Read daemon password from file | ❌ Not Implemented | | | `--password-file=FILE` | Read daemon password from file | ❌ Not Implemented | |
@@ -266,7 +266,6 @@ Ranked by user demand, implementation complexity, and interoperability impact:
| `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) | | `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) |
| `-c [level]` | zstd compression level (1-22) | | `-c [level]` | zstd compression level (1-22) |
| `--chunk-size` | Configurable chunk size | | `--chunk-size` | Configurable chunk size |
| `--queue-size` | Pipeline queue capacity |
| `--tls` | TLS encryption (mutual auth) | | `--tls` | TLS encryption (mutual auth) |
| `--fastsync-server-path` | Path to fastsync-server binary | | `--fastsync-server-path` | Path to fastsync-server binary |
| `--server-host` / `--server-port` | Direct TCP connection | | `--server-host` / `--server-port` | Direct TCP connection |
+21 -277
View File
@@ -1,13 +1,16 @@
#include "client_send.h" #include "client_send.h"
#include "client_validation.h"
#include "config.h" #include "config.h"
#include "delta.h" #include "delta.h"
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include "transport_tcp.h" #include "transport_tcp.h"
#include "transport_tls.h" #include "transport_tls.h"
#include "usage.h"
#include "utils.h" #include "utils.h"
#include <errno.h> #include <errno.h>
#include <limits.h> #include <limits.h>
#include <stdbool.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -25,19 +28,6 @@ static void parse_environment(const char** out_env_source, const char** out_env_
} }
#endif #endif
/* Parse a string as a positive integer, returning true on success. */
static bool parse_positive_int(const char* s, int* out_val) {
if (!s || *s == '\0')
return false;
char* endptr;
errno = 0;
long val = strtol(s, &endptr, 10);
if (errno != 0 || *endptr != '\0' || val <= 0 || val > INT_MAX)
return false;
*out_val = (int)val;
return true;
}
/* Parse a string as a non-negative integer, returning true on success. */ /* Parse a string as a non-negative integer, returning true on success. */
static bool parse_nonneg_int(const char* s, int* out_val) { static bool parse_nonneg_int(const char* s, int* out_val) {
if (!s || *s == '\0') if (!s || *s == '\0')
@@ -51,7 +41,20 @@ static bool parse_nonneg_int(const char* s, int* out_val) {
return true; return true;
} }
/* Duplicate a string argument into *dest, freeing the old value. Returns 0 on success, -1 on /* Parse a string as a positive integer, returning true on success. */
static bool parse_positive_int(const char* s, int* out_val) {
int temp;
if (!parse_nonneg_int(s, &temp)) {
return false;
}
if (temp == 0) {
return false;
}
*out_val = temp;
return true;
}
/* Duplicate a string argument into *dest, freeing the old value. Returns true on success, false on
* failure. */ * failure. */
static int set_string_option(char** dest, const char* value, const char* option_name) { static int set_string_option(char** dest, const char* value, const char* option_name) {
char* dup = str_dup(value); char* dup = str_dup(value);
@@ -64,7 +67,7 @@ static int set_string_option(char** dest, const char* value, const char* option_
return 0; return 0;
} }
/* Parse a string as a positive integer into *dest. Returns 0 on success, -1 on error. */ /* Parse a string as a positive integer into *dest. Returns true on success, false on error. */
static int set_positive_int_option(int* dest, const char* value, const char* option_name) { static int set_positive_int_option(int* dest, const char* value, const char* option_name) {
if (!parse_positive_int(value, dest)) { if (!parse_positive_int(value, dest)) {
fprintf(stderr, "Error: %s must be a positive integer\n", option_name); fprintf(stderr, "Error: %s must be a positive integer\n", option_name);
@@ -73,7 +76,7 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt
return 0; return 0;
} }
/* Parse a string as a non-negative integer into *dest. Returns 0 on success, -1 on error. */ /* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */
static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) {
if (!parse_nonneg_int(value, dest)) { if (!parse_nonneg_int(value, dest)) {
fprintf(stderr, "Error: %s must be a non-negative integer\n", option_name); fprintf(stderr, "Error: %s must be a non-negative integer\n", option_name);
@@ -82,7 +85,6 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio
return 0; return 0;
} }
static void print_usage(void);
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); static int read_patterns_from_file(const char* filepath, char*** patterns, int* count);
/* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ /* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */
@@ -103,12 +105,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) { } else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) {
config->dry_run = true; config->dry_run = true;
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
if (!parse_positive_int(argv[++i], &config->ssh_port)) { if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0)
fprintf(stderr, "Error: invalid --port/-p value: %s\n", argv[i]);
return -1; return -1;
}
if (config->ssh_port > 65535) { if (config->ssh_port > 65535) {
fprintf(stderr, "Error: SSH port must be 1-65535\n"); log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535\n");
return -1; return -1;
} }
} else if (strcmp(argv[i], "--delete") == 0) { } else if (strcmp(argv[i], "--delete") == 0) {
@@ -275,9 +275,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) {
if (set_positive_int_option(&config->contimeout, argv[++i], "--contimeout") != 0) if (set_positive_int_option(&config->contimeout, argv[++i], "--contimeout") != 0)
return -1; return -1;
} else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 ||
strcmp(argv[i], "--silent") == 0) {
config->quiet = true;
} else if (strcmp(argv[i], "--backup") == 0) { } else if (strcmp(argv[i], "--backup") == 0) {
config->backup = true; config->backup = true;
} else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) {
@@ -301,9 +298,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
} }
config->log_file = lf; config->log_file = lf;
log_set_file(lf); log_set_file(lf);
} else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) {
if (set_positive_int_option(&config->queue_size, argv[++i], "--queue-size") != 0)
return -1;
} else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) {
if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) !=
0) 0)
@@ -328,118 +322,21 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
config->safe_links = true; config->safe_links = true;
} else if (strcmp(argv[i], "--copy-unsafe-links") == 0) { } else if (strcmp(argv[i], "--copy-unsafe-links") == 0) {
config->copy_unsafe_links = true; config->copy_unsafe_links = true;
} else if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--hard-links") == 0) {
config->preserve_hard_links = true;
} else if (strcmp(argv[i], "-A") == 0 || strcmp(argv[i], "--acls") == 0) {
config->preserve_acls = true;
} else if (strcmp(argv[i], "-X") == 0 || strcmp(argv[i], "--xattrs") == 0) {
config->preserve_xattrs = true;
} else if (strcmp(argv[i], "-D") == 0 || strcmp(argv[i], "--devices") == 0) {
config->preserve_devices = true;
} else if (strcmp(argv[i], "-S") == 0 || strcmp(argv[i], "--sparse") == 0) { } else if (strcmp(argv[i], "-S") == 0 || strcmp(argv[i], "--sparse") == 0) {
config->preserve_sparse = true; config->preserve_sparse = true;
} else if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--itemize-changes") == 0) {
config->itemize_changes = true;
} else if (strcmp(argv[i], "--out-format") == 0 && i + 1 < argc) {
if (set_string_option(&config->out_format, argv[++i], "--out-format") != 0)
return -1;
} else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) {
if (set_nonneg_int_option(&config->info_level, argv[++i], "--info") != 0)
return -1;
} else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) {
if (set_nonneg_int_option(&config->debug_level, argv[++i], "--debug") != 0)
return -1;
} else if (strcmp(argv[i], "--list-only") == 0) {
config->list_only = true;
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--human-readable") == 0) {
config->human_readable = true;
} else if (strcmp(argv[i], "-u") == 0 || strcmp(argv[i], "--update") == 0) {
config->update = true;
} else if (strcmp(argv[i], "--inplace") == 0) { } else if (strcmp(argv[i], "--inplace") == 0) {
config->inplace = true; config->inplace = true;
} else if (strcmp(argv[i], "--append") == 0) {
config->append = true;
} else if (strcmp(argv[i], "--append-verify") == 0) {
config->append_verify = true;
} else if (strcmp(argv[i], "--delete-excluded") == 0) {
config->delete_excluded = true;
} else if (strcmp(argv[i], "--delete-after") == 0) {
config->delete_after = true;
} else if (strcmp(argv[i], "--max-delete") == 0 && i + 1 < argc) {
if (set_nonneg_int_option(&config->max_delete, argv[++i], "--max-delete") != 0)
return -1;
} else if (strcmp(argv[i], "--filter") == 0 && i + 1 < argc) {
if (!config->filters)
config->filters = array_list_create(free);
char* dup = str_dup(argv[++i]);
if (!dup)
return -1;
array_list_add(config->filters, dup);
} else if (strcmp(argv[i], "--files-from") == 0 && i + 1 < argc) {
if (set_string_option(&config->files_from, argv[++i], "--files-from") != 0)
return -1;
} else if (strcmp(argv[i], "--cvs-exclude") == 0) {
config->cvs_exclude = true;
} else if (strcmp(argv[i], "--prune-empty-dirs") == 0) {
config->prune_empty_dirs = true;
} else if (strcmp(argv[i], "-R") == 0 || strcmp(argv[i], "--relative") == 0) {
config->relative = true;
} else if (strcmp(argv[i], "-e") == 0 || strcmp(argv[i], "--rsh") == 0) {
if (i + 1 < argc) {
if (set_string_option(&config->rsh_command, argv[++i], "-e/--rsh") != 0)
return -1;
} else {
fprintf(stderr, "Error: -e/--rsh requires a command argument\n");
return -1;
}
} else if (strcmp(argv[i], "--rsync-path") == 0 && i + 1 < argc) {
if (set_string_option(&config->rsync_path, argv[++i], "--rsync-path") != 0)
return -1;
} else if (strcmp(argv[i], "--temp-dir") == 0 && i + 1 < argc) {
if (set_string_option(&config->temp_dir, argv[++i], "--temp-dir") != 0)
return -1;
} else if (strcmp(argv[i], "--compare-dest") == 0 && i + 1 < argc) {
if (set_string_option(&config->compare_dest, argv[++i], "--compare-dest") != 0)
return -1;
} else if (strcmp(argv[i], "--copy-dest") == 0 && i + 1 < argc) {
if (set_string_option(&config->copy_dest, argv[++i], "--copy-dest") != 0)
return -1;
} else if (strcmp(argv[i], "--link-dest") == 0 && i + 1 < argc) {
if (set_string_option(&config->link_dest, argv[++i], "--link-dest") != 0)
return -1;
} else if (strcmp(argv[i], "--partial-dir") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--partial-dir") == 0 && i + 1 < argc) {
if (set_string_option(&config->partial_dir, argv[++i], "--partial-dir") != 0) if (set_string_option(&config->partial_dir, argv[++i], "--partial-dir") != 0)
return -1; return -1;
} else if (strcmp(argv[i], "--suffix") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--suffix") == 0 && i + 1 < argc) {
if (set_string_option(&config->suffix, argv[++i], "--suffix") != 0) if (set_string_option(&config->suffix, argv[++i], "--suffix") != 0)
return -1; return -1;
} else if (strcmp(argv[i], "--delete-before") == 0) {
config->delete_before = true;
} else if (strcmp(argv[i], "-T") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "-T") == 0 && i + 1 < argc) {
if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0)
return -1; return -1;
} else if (strcmp(argv[i], "--address") == 0 && i + 1 < argc) {
if (set_string_option(&config->address, argv[++i], "--address") != 0)
return -1;
} else if (strcmp(argv[i], "--bind-address") == 0 && i + 1 < argc) {
if (set_string_option(&config->bind_address, argv[++i], "--bind-address") != 0)
return -1;
} else if (strcmp(argv[i], "--ipv6") == 0) {
config->ipv6 = true;
} else if (strcmp(argv[i], "--ipv4") == 0) {
config->ipv4 = true;
} else if (strcmp(argv[i], "--daemon") == 0) {
config->daemon = true;
} else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) {
if (set_string_option(&config->daemon_config, argv[++i], "--config") != 0)
return -1;
} else if (strcmp(argv[i], "--server") == 0) {
config->server_mode = true;
} else if (strcmp(argv[i], "--checksum") == 0) { } else if (strcmp(argv[i], "--checksum") == 0) {
config->checksum = true; config->checksum = true;
} else if (strcmp(argv[i], "--compress-choice") == 0 && i + 1 < argc) {
if (set_string_option(&config->compress_choice, argv[++i], "--compress-choice") != 0)
return -1;
} else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) {
if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0) if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0)
return -1; return -1;
@@ -464,159 +361,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
return 0; return 0;
} }
#ifndef FASTSYNC_TEST_BUILD
/* Validate config after parsing. Returns true if valid. */
static bool validate_config(const Config* config) {
if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n");
print_usage();
return false;
}
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk "
"serialization)\n");
return false;
}
if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return false;
}
if (config->use_incremental && config->use_chunk_serialization) {
fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n");
return false;
}
if (config->use_delta && !config->use_incremental) {
fprintf(stderr, "Error: --delta requires --incremental\n");
return false;
}
if (config->use_delta && config->use_chunk_serialization) {
fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n");
return false;
}
if (config->use_delta && config->use_sendfile) {
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
return false;
}
if (config->use_tls) {
if (!config->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n");
return false;
}
}
return true;
}
#endif /* FASTSYNC_TEST_BUILD */
static void print_usage(void) {
printf("Usage:\n");
printf(" fastsync [options] <source> <destination>\n");
printf(" fastsync [options] --source-dir <src> --dest-dir <dst>\n");
printf("\n");
printf("Destination formats:\n");
printf(" user@host:/path SSH transport (rsync-style)\n");
printf(" host:/path SSH transport (current user)\n");
printf(" /local/path TCP transport (requires server on localhost:8080)\n");
printf("\n");
printf("Options:\n");
printf(" -c [level] Enable compression (level 1-22, default 5)\n");
printf(" -z [level] Alias for -c\n");
printf(" -a, --archive Archive mode (-c -m -M)\n");
printf(" -n, --dry-run Show what would be transferred\n");
printf(" -p <port> SSH port (default: 22)\n");
printf(" --progress Show transfer progress\n");
printf(" --delete Delete files on receiver not in source\n");
printf(" --exclude <pattern> Exclude files matching pattern\n");
printf(" --include <pattern> Only include files matching pattern\n");
printf(" --exclude-from <file> Read exclude patterns from file\n");
printf(" --include-from <file> Read include patterns from file\n");
printf(" --max-size <n> Skip files larger than n bytes\n");
printf(" --min-size <n> Skip files smaller than n bytes\n");
printf(" --incremental Skip files unchanged since last transfer\n");
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
DELTA_BLOCK_SIZE_DEFAULT);
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
DELTA_MAX_FILE_SIZE);
printf(" -m Enable multithreading\n");
printf(" -s Enable chunk serialization\n");
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
printf(" -v, --verbose Enable debug logging\n");
printf(" -M, --preserve Preserve file metadata\n");
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
printf(" --source-dir <path> Source directory\n");
printf(" --dest-dir <path> Destination directory\n");
printf(" --save-to-disk Write received files to disk\n");
printf(" --server-host <ip> Server IP address (default: 127.0.0.1)\n");
printf(" --server-port <n> Server port (default: 8080)\n");
printf(" --bwlimit <KB/s> Bandwidth limit in kilobytes per second\n");
printf(" --tls Enable TLS encryption\n");
printf(" --cert <path> TLS certificate file (PEM)\n");
printf(" --key <path> TLS private key file (PEM)\n");
printf(" --ca <path> TLS CA certificate file (PEM)\n");
printf(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
printf(" -T <sec> Alias for --timeout\n");
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\n");
printf(" --address <host> Server hostname/IP to connect to\n");
printf(" --bind-address <ip> Bind to specific local address\n");
printf(" --ipv6 Prefer IPv6 connections\n");
printf(" --ipv4 Prefer IPv4 connections\n");
printf(" -q, --quiet Suppress non-error output\n");
printf(" --silent Alias for --quiet\n");
printf(" --backup Backup existing files before overwriting\n");
printf(" --backup-dir <dir> Directory for backups (requires --backup)\n");
printf(" --suffix <str> Backup suffix (default: ~)\n");
printf(" --stats Print transfer statistics at end\n");
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
printf(" --log-file <path> Write log messages to file\n");
printf(" --queue-size <n> Queue capacity for multithreaded mode (default: 100)\n");
printf(" --partial Keep partial files on interrupted transfer\n");
printf(" --partial-dir <dir> Directory for partial files\n");
printf(" --fastsync-server-path <path>\n");
printf(" Path to fastsync-server on remote (default: fastsync-server)\n");
printf(" -l, --links Copy symlinks as symlinks\n");
printf(" --copy-links Transform symlinks into referent files\n");
printf(" --safe-links Skip symlinks that point outside transfer tree\n");
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
printf(" -H, --hard-links Preserve hard links\n");
printf(" -A, --acls Preserve ACLs\n");
printf(" -X, --xattrs Preserve extended attributes\n");
printf(" -D, --devices Preserve device files\n");
printf(" -S, --sparse Handle sparse files efficiently\n");
printf(" -i, --itemize-changes Show per-file change summary\n");
printf(" --out-format <fmt> Custom output format string\n");
printf(" --info <flags> Info verbosity level\n");
printf(" --debug <flags> Debug verbosity level\n");
printf(" --list-only List files without transferring\n");
printf(" -h, --human-readable Human-readable numbers\n");
printf(" -u, --update Skip files newer on destination\n");
printf(" --inplace Update files in-place (no temp+rename)\n");
printf(" --append Append data to shorter files\n");
printf(" --append-verify Append with verify\n");
printf(" --delete-before Delete before transfer\n");
printf(" --delete-excluded Also delete excluded files\n");
printf(" --delete-after Delete after transfer, not before\n");
printf(" --max-delete <n> Maximum number of files to delete\n");
printf(" --filter <rule> Add file filtering rule\n");
printf(" --files-from <file> Read file list from file\n");
printf(" --cvs-exclude Auto-ignore CVS files\n");
printf(" --prune-empty-dirs Omit empty directories from transfer\n");
printf(" -R, --relative Use relative paths\n");
printf(" -e, --rsh <cmd> Specify remote shell\n");
printf(" --rsync-path <path> Path to remote binary\n");
printf(" --daemon Run in daemon mode\n");
printf(" --config <path> Path to configuration file\n");
printf(" --server Run in server mode\n");
printf(" --checksum Skip files based on checksum, not mod-time/size\n");
printf(" --compress-choice <alg> Compression algorithm (default: zstd)\n");
printf(" --compress-level <n> Compression level (default: 5)\n");
printf(" --temp-dir <dir> Temporary directory for files\n");
printf(" --compare-dest <dir> Compare destination\n");
printf(" --copy-dest <dir> Copy destination\n");
printf(" --link-dest <dir> Link destination\n");
printf(" --help Show this help\n");
printf(" -V, --version Show version\n");
}
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) {
FILE* fp = fopen(filepath, "r"); FILE* fp = fopen(filepath, "r");
if (!fp) { if (!fp) {
+209 -169
View File
@@ -28,13 +28,106 @@
/* Forward declaration for progress-reporting thread used in multithreaded send. */ /* Forward declaration for progress-reporting thread used in multithreaded send. */
static int progress_thread_fn(void* arg); static int progress_thread_fn(void* arg);
static ScannerOptions scanner_options_from_config(const Config* config, int num_threads) {
ScannerOptions options = {
config->use_metadata, config->chunk_size, config->exclude_patterns,
config->exclude_count, config->include_patterns, config->include_count,
config->max_size, config->min_size, config->max_depth,
num_threads, config->follow_symlinks, config->copy_links,
config->safe_links, config->copy_unsafe_links, config->checksum};
return options;
}
/* Select the configured transport for both transfer execution paths. */
static Client* connect_transfer_client(const Config* config) {
if (config->transport == TRANSPORT_SSH) {
if (config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return NULL;
}
return client_connect_ssh(config->ssh_destination, config->ssh_port,
config->fastsync_server_path);
}
Client* client = client_create();
if (!client)
return NULL;
bool connected;
if (config->use_tls) {
connected = client_connect_tls(client, config->server_host, config->server_port,
config->tls_cert, config->tls_key, config->tls_ca);
} else {
connected = client_connect(client, config->server_host, config->server_port);
}
if (!connected) {
client_delete(client);
return NULL;
}
return client;
}
static void disconnect_transfer_client(Client* client) {
if (!client)
return;
client_disconnect(client);
client_delete(client);
}
static ArrayList* create_transfer_manifest(const Config* config) {
return config->use_delete ? array_list_create(free) : NULL;
}
static bool add_chunk_to_manifest(ArrayList* manifest, const Chunk* chunk) {
if (!manifest)
return true;
for (int i = 0; i < chunk->element_count; i++) {
const char* path = chunk->items[i]->path;
if (*path == '/')
path++;
char* entry = str_dup(path);
if (!entry) {
log_message(LOG_LEVEL_ERROR, "Failed to allocate manifest entry");
return false;
}
if (!array_list_add(manifest, entry)) {
free(entry);
return false;
}
}
return true;
}
static bool finalize_transfer(Client* client) {
Status status;
return send_status(client->file_descriptor, STATUS_FINISHED) &&
receive_status(client->file_descriptor, &status) && status == STATUS_OK;
}
static void mark_sender_done(PipelineContextSender* context) {
mtx_lock(&context->mutex_progress);
context->sender_done = true;
mtx_unlock(&context->mutex_progress);
}
static void pipeline_cancel(PipelineContextSender* context) {
mtx_lock(&context->mutex_scanner);
mtx_lock(&context->mutex_loader);
atomic_store(&context->cancelled, true);
context->scanner_done = true;
context->loader_done = true;
cnd_broadcast(&context->condition_not_full_scanner);
cnd_broadcast(&context->condition_not_empty_scanner);
cnd_broadcast(&context->condition_not_full_loader);
cnd_broadcast(&context->condition_not_empty_loader);
mtx_unlock(&context->mutex_loader);
mtx_unlock(&context->mutex_scanner);
}
/* Print dry-run manifest showing files that would be transferred. Returns 0 on success. */ /* Print dry-run manifest showing files that would be transferred. Returns 0 on success. */
static int send_dry_run_manifest(Config* config) { static int send_dry_run_manifest(const Config* config) {
DirectoryScanner* scanner = directory_scanner_create( ScannerOptions options = scanner_options_from_config(config, 0);
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, DirectoryScanner* scanner =
config->exclude_count, config->include_patterns, config->include_count, config->max_size, directory_scanner_create_with_options(config->send_directory, &options);
config->min_size, config->max_depth, config->follow_symlinks, config->copy_links,
config->safe_links, config->copy_unsafe_links, config->checksum);
if (!scanner) if (!scanner)
return -1; return -1;
Chunk* chunk; Chunk* chunk;
@@ -67,7 +160,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 +173,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 +276,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 +302,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;
@@ -270,49 +370,22 @@ int send_chunk(Client* client, Chunk* chunk, Config* config) {
static int send_chunks_multithreaded(void* pipeline_context) { static int send_chunks_multithreaded(void* pipeline_context) {
PipelineContextSender* context = (PipelineContextSender*)pipeline_context; PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
Client* client; Client* client = connect_transfer_client(context->config);
if (context->config->transport == TRANSPORT_SSH) { if (!client) {
if (context->config->use_sendfile) { if (context->config->transport == TRANSPORT_TCP)
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); fprintf(stderr, "Error: could not connect to server%s\n",
mtx_lock(&context->mutex_progress); context->config->use_tls ? " via TLS" : "");
context->sender_done = true; mark_sender_done(context);
mtx_unlock(&context->mutex_progress); return thrd_error;
return 1;
}
client = client_connect_ssh(context->config->ssh_destination, context->config->ssh_port,
context->config->fastsync_server_path);
} else if (context->config->use_tls) {
client = client_create();
if (!client || !client_connect_tls(client, context->config->server_host,
context->config->server_port, context->config->tls_cert,
context->config->tls_key, context->config->tls_ca)) {
if (client)
client_delete(client);
fprintf(stderr, "Error: could not connect to server via TLS\n");
mtx_lock(&context->mutex_progress);
context->sender_done = true;
mtx_unlock(&context->mutex_progress);
return thrd_error;
}
} else {
client = client_create();
if (!client ||
!client_connect(client, context->config->server_host, context->config->server_port)) {
if (client)
client_delete(client);
fprintf(stderr, "Error: could not connect to server\n");
mtx_lock(&context->mutex_progress);
context->sender_done = true;
mtx_unlock(&context->mutex_progress);
return thrd_error;
}
} }
ProtocolSession session;
protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
protocol_session_set_ssl(&session, (SSL*)client->ssl);
protocol_session_bind(&session);
if (!config_send(client->file_descriptor, context->config)) { if (!config_send(client->file_descriptor, context->config)) {
client_disconnect(client); disconnect_transfer_client(client);
client_delete(client); mark_sender_done(context);
mtx_lock(&context->mutex_progress); protocol_session_unbind();
context->sender_done = true;
mtx_unlock(&context->mutex_progress);
return thrd_error; return thrd_error;
} }
@@ -325,33 +398,26 @@ static int send_chunks_multithreaded(void* pipeline_context) {
if (send_delete_manifest(client->file_descriptor, context->manifest) != 0) if (send_delete_manifest(client->file_descriptor, context->manifest) != 0)
goto send_fail; goto send_fail;
} }
if (!send_status(client->file_descriptor, STATUS_FINISHED)) bool ok = finalize_transfer(client);
goto send_fail; disconnect_transfer_client(client);
Status s; mark_sender_done(context);
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK; protocol_session_unbind();
client_disconnect(client);
client_delete(client);
mtx_lock(&context->mutex_progress);
context->sender_done = true;
mtx_unlock(&context->mutex_progress);
return ok ? thrd_success : thrd_error; return ok ? thrd_success : thrd_error;
send_fail: send_fail:
client_disconnect(client); pipeline_cancel(context);
client_delete(client); disconnect_transfer_client(client);
mtx_lock(&context->mutex_progress); mark_sender_done(context);
context->sender_done = true; protocol_session_unbind();
mtx_unlock(&context->mutex_progress);
return thrd_error; return thrd_error;
} }
if (send_chunk(client, current_chunk, context->config) != 0) { if (send_chunk(client, current_chunk, context->config) != 0) {
fprintf(stderr, "Error: unexpected error while sending chunk\n"); fprintf(stderr, "Error: unexpected error while sending chunk\n");
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
client_disconnect(client); pipeline_cancel(context);
client_delete(client); disconnect_transfer_client(client);
mtx_lock(&context->mutex_progress); mark_sender_done(context);
context->sender_done = true; protocol_session_unbind();
mtx_unlock(&context->mutex_progress);
return thrd_error; return thrd_error;
} }
if (context->config->show_progress) { if (context->config->show_progress) {
@@ -370,48 +436,48 @@ static int send_chunks_multithreaded(void* pipeline_context) {
static int scan_directory_multithreaded(void* pipeline_context) { static int scan_directory_multithreaded(void* pipeline_context) {
PipelineContextSender* context = (PipelineContextSender*)pipeline_context; PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
ParallelScanner* scanner = parallel_scanner_create( ScannerOptions options = scanner_options_from_config(context->config, 4);
context->config->send_directory, context->config->use_metadata, context->config->chunk_size, ParallelScanner* scanner =
context->config->exclude_patterns, context->config->exclude_count, parallel_scanner_create_with_options(context->config->send_directory, &options);
context->config->include_patterns, context->config->include_count, context->config->max_size,
context->config->min_size, context->config->max_depth, 4, context->config->follow_symlinks,
context->config->copy_links, context->config->safe_links, context->config->copy_unsafe_links,
context->config->checksum);
Chunk* current_chunk; Chunk* current_chunk;
if (scanner == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to create parallel scanner");
pipeline_cancel(context);
return thrd_error;
}
while ((current_chunk = parallel_scanner_next(scanner)) != NULL) { while ((current_chunk = parallel_scanner_next(scanner)) != NULL) {
if (context->config->use_delete) { if (context->config->use_delete) {
mtx_lock(&context->mutex_scanner); mtx_lock(&context->mutex_scanner);
for (int i = 0; i < current_chunk->element_count; i++) { bool manifest_ok = add_chunk_to_manifest(context->manifest, current_chunk);
const char* p = current_chunk->items[i]->path;
if (*p == '/')
p++;
char* manifest_entry = str_dup(p);
if (!manifest_entry) {
log_message(LOG_LEVEL_ERROR, "Failed to allocate manifest entry");
mtx_unlock(&context->mutex_scanner);
context->cancelled = true;
cnd_broadcast(&context->condition_not_full_scanner);
cnd_broadcast(&context->condition_not_empty_scanner);
parallel_scanner_destroy(scanner);
return thrd_error;
}
array_list_add(context->manifest, manifest_entry);
}
mtx_unlock(&context->mutex_scanner); mtx_unlock(&context->mutex_scanner);
if (!manifest_ok) {
pipeline_cancel(context);
chunk_destroy(current_chunk);
parallel_scanner_destroy(scanner);
return thrd_error;
}
} }
if (!queue_enqueue_multithreaded_cancel( if (!queue_enqueue_multithreaded_cancel(
context->queue_scanner, current_chunk, &context->mutex_scanner, context->queue_scanner, current_chunk, &context->mutex_scanner,
&context->condition_not_empty_scanner, &context->condition_not_full_scanner, &context->condition_not_empty_scanner, &context->condition_not_full_scanner,
&context->cancelled)) { &context->cancelled)) {
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
context->cancelled = true; pipeline_cancel(context);
cnd_broadcast(&context->condition_not_full_scanner);
cnd_broadcast(&context->condition_not_empty_scanner);
parallel_scanner_destroy(scanner); parallel_scanner_destroy(scanner);
return thrd_error; return thrd_error;
} }
} }
if (parallel_scanner_failed(scanner)) {
parallel_scanner_destroy(scanner);
mtx_lock(&context->mutex_scanner);
context->scanner_done = true;
cnd_broadcast(&context->condition_not_empty_scanner);
cnd_broadcast(&context->condition_not_full_scanner);
mtx_unlock(&context->mutex_scanner);
pipeline_cancel(context);
return thrd_error;
}
mtx_lock(&context->mutex_scanner); mtx_lock(&context->mutex_scanner);
context->scanner_done = true; context->scanner_done = true;
cnd_signal(&context->condition_not_empty_scanner); cnd_signal(&context->condition_not_empty_scanner);
@@ -451,7 +517,7 @@ static int load_files_multithreaded(void* pipeline_context) {
&context->condition_not_full_loader, &context->condition_not_full_loader,
&context->cancelled)) { &context->cancelled)) {
chunk_destroy(chunk); chunk_destroy(chunk);
context->cancelled = true; atomic_store(&context->cancelled, true);
cnd_broadcast(&context->condition_not_full_loader); cnd_broadcast(&context->condition_not_full_loader);
cnd_broadcast(&context->condition_not_empty_loader); cnd_broadcast(&context->condition_not_empty_loader);
return thrd_error; return thrd_error;
@@ -499,75 +565,48 @@ int send_files(Config* config) {
if (config->dry_run) if (config->dry_run)
return send_dry_run_manifest(config); return send_dry_run_manifest(config);
Client* client; Client* client = connect_transfer_client(config);
if (config->transport == TRANSPORT_SSH) { if (!client) {
if (config->use_sendfile) { if (config->transport == TRANSPORT_TCP)
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); fprintf(stderr, "Error: could not connect to server%s\n", config->use_tls ? " via TLS" : "");
return 1;
}
client =
client_connect_ssh(config->ssh_destination, config->ssh_port, config->fastsync_server_path);
if (!client)
return 1;
} else if (config->use_tls) {
client = client_create();
if (!client || !client_connect_tls(client, config->server_host, config->server_port,
config->tls_cert, config->tls_key, config->tls_ca)) {
if (client) {
client_disconnect(client);
client_delete(client);
}
fprintf(stderr, "Error: could not connect to server via TLS\n");
return 1;
}
} else {
client = client_create();
if (!client || !client_connect(client, config->server_host, config->server_port)) {
if (client) {
client_disconnect(client);
client_delete(client);
}
fprintf(stderr, "Error: could not connect to server\n");
return 1;
}
}
if (!config_send(client->file_descriptor, config)) {
client_disconnect(client);
client_delete(client);
return 1; return 1;
} }
DirectoryScanner* scanner = directory_scanner_create( ProtocolSession session;
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, protocol_session_init(&session, client->file_descriptor, client->file_descriptor);
config->exclude_count, config->include_patterns, config->include_count, config->max_size, protocol_session_set_ssl(&session, (SSL*)client->ssl);
config->min_size, config->max_depth, config->follow_symlinks, config->copy_links, protocol_session_bind(&session);
config->safe_links, config->copy_unsafe_links, config->checksum); if (!config_send(client->file_descriptor, config)) {
disconnect_transfer_client(client);
protocol_session_unbind();
return 1;
}
ScannerOptions scanner_options = scanner_options_from_config(config, 0);
DirectoryScanner* scanner =
directory_scanner_create_with_options(config->send_directory, &scanner_options);
Chunk* current_chunk; Chunk* current_chunk;
unsigned long long total_bytes = 0; unsigned long long total_bytes = 0;
int total_files = 0; int total_files = 0;
time_t last_progress = 0; time_t last_progress = 0;
time_t start = time(NULL); time_t start = time(NULL);
ArrayList* manifest = config->use_delete ? array_list_create(free) : NULL; ArrayList* manifest = create_transfer_manifest(config);
if (!scanner || (config->use_delete && !manifest)) {
if (scanner)
directory_scanner_destroy(scanner);
if (manifest)
array_list_delete(manifest);
disconnect_transfer_client(client);
protocol_session_unbind();
return 1;
}
while ((current_chunk = directory_scanner_next(scanner)) != NULL) { while ((current_chunk = directory_scanner_next(scanner)) != NULL) {
unsigned long long chunk_bytes = 0; unsigned long long chunk_bytes = 0;
for (int i = 0; i < current_chunk->element_count; i++) { for (int i = 0; i < current_chunk->element_count; i++) {
chunk_bytes += current_chunk->items[i]->data->size; chunk_bytes += current_chunk->items[i]->data->size;
total_files++; total_files++;
if (manifest) { }
const char* p = current_chunk->items[i]->path; if (!add_chunk_to_manifest(manifest, current_chunk)) {
if (*p == '/') chunk_destroy(current_chunk);
p++; goto send_fail;
char* manifest_entry = str_dup(p);
if (!manifest_entry) {
log_message(LOG_LEVEL_ERROR, "Failed to allocate manifest entry");
chunk_destroy(current_chunk);
array_list_delete(manifest);
directory_scanner_destroy(scanner);
client_disconnect(client);
client_delete(client);
return 1;
}
array_list_add(manifest, manifest_entry);
}
} }
if (!config->use_sendfile) { if (!config->use_sendfile) {
for (int i = 0; i < current_chunk->element_count; i++) { for (int i = 0; i < current_chunk->element_count; i++) {
@@ -583,6 +622,9 @@ int send_files(Config* config) {
if (send_chunk(client, current_chunk, config) != 0) { if (send_chunk(client, current_chunk, config) != 0) {
log_message(LOG_LEVEL_ERROR, "Failed to send chunk"); log_message(LOG_LEVEL_ERROR, "Failed to send chunk");
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
if (manifest)
array_list_delete(manifest);
manifest = NULL;
break; break;
} }
if (config->show_progress) { if (config->show_progress) {
@@ -598,17 +640,17 @@ int send_files(Config* config) {
} }
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
} }
if (directory_scanner_failed(scanner) || (config->use_delete && manifest == NULL))
goto send_fail;
if (config->use_delete) { if (config->use_delete) {
if (send_delete_manifest(client->file_descriptor, manifest) != 0) { if (send_delete_manifest(client->file_descriptor, manifest) != 0) {
array_list_delete(manifest); array_list_delete(manifest);
goto send_fail; goto send_fail;
} }
array_list_delete(manifest); array_list_delete(manifest);
manifest = NULL;
} }
if (!send_status(client->file_descriptor, STATUS_FINISHED)) bool ok = finalize_transfer(client);
goto send_fail;
Status s;
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK;
double elapsed_total = difftime(time(NULL), start); double elapsed_total = difftime(time(NULL), start);
if (config->show_progress) { if (config->show_progress) {
double rate = elapsed_total > 0 ? total_bytes / (1048576.0 * elapsed_total) : 0; double rate = elapsed_total > 0 ? total_bytes / (1048576.0 * elapsed_total) : 0;
@@ -620,14 +662,16 @@ int send_files(Config* config) {
rate); rate);
} }
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
client_disconnect(client); disconnect_transfer_client(client);
client_delete(client); protocol_session_unbind();
return ok ? 0 : 1; return ok ? 0 : 1;
send_fail: send_fail:
if (manifest)
array_list_delete(manifest);
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
client_disconnect(client); disconnect_transfer_client(client);
client_delete(client); protocol_session_unbind();
return 1; return 1;
} }
@@ -663,7 +707,7 @@ int send_files_multithreaded(Config* config) {
return 1; return 1;
} }
if (config->use_delete) if (config->use_delete)
context->manifest = array_list_create(free); context->manifest = create_transfer_manifest(config);
thrd_t scanner, loader, sender; thrd_t scanner, loader, sender;
bool scanner_created = false; bool scanner_created = false;
@@ -678,14 +722,10 @@ int send_files_multithreaded(Config* config) {
if (!scanner_created || !loader_created || !sender_created) { if (!scanner_created || !loader_created || !sender_created) {
perror("Error creating threads.\n"); perror("Error creating threads.\n");
context->cancelled = true; pipeline_cancel(context);
context->scanner_done = true; mtx_lock(&context->mutex_progress);
context->loader_done = true;
context->sender_done = true; context->sender_done = true;
cnd_broadcast(&context->condition_not_full_scanner); mtx_unlock(&context->mutex_progress);
cnd_broadcast(&context->condition_not_empty_scanner);
cnd_broadcast(&context->condition_not_full_loader);
cnd_broadcast(&context->condition_not_empty_loader);
if (sender_created) if (sender_created)
thrd_join(sender, NULL); thrd_join(sender, NULL);
if (loader_created) if (loader_created)
+50
View File
@@ -0,0 +1,50 @@
#include "client_validation.h"
#include "usage.h"
#include <stdio.h>
/* Validate config after parsing. Returns true if valid. */
bool validate_config(const Config* config) {
if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n");
print_usage();
return false;
}
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk "
"serialization)\n");
return false;
}
if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return false;
}
if (config->use_incremental && config->use_chunk_serialization) {
fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n");
return false;
}
if (config->use_delta && !config->use_incremental) {
fprintf(stderr, "Error: --delta requires --incremental\n");
return false;
}
if (config->use_delta && config->use_chunk_serialization) {
fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n");
return false;
}
if (config->use_delta && config->use_sendfile) {
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
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->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n");
return false;
}
}
return true;
}
+9
View File
@@ -0,0 +1,9 @@
#ifndef CLIENT_VALIDATION_H
#define CLIENT_VALIDATION_H
#include "config.h"
#include <stdbool.h>
bool validate_config(const Config* config);
#endif
+337 -255
View File
@@ -11,6 +11,7 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <threads.h> #include <threads.h>
#include <unistd.h> #include <unistd.h>
#include <limits.h>
typedef struct { typedef struct {
char* path; char* path;
@@ -38,34 +39,123 @@ static DirEntry* dir_entry_create(const char* path, int depth) {
return de; return de;
} }
DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_metadata, static bool safe_relative_link(const char* source_root, const char* containing_dir,
unsigned long long chunk_size, char** exclude_patterns, const char* link_target) {
int exclude_count, char** include_patterns, char root[PATH_MAX];
int include_count, unsigned long long max_size, if (!realpath(source_root, root))
unsigned long long min_size, int max_depth, return false;
bool follow_symlinks, bool copy_links, bool safe_links, char* joined = path_cat(containing_dir, link_target);
bool copy_unsafe_links, bool checksum) { char resolved[PATH_MAX];
DirectoryScanner* scanner = malloc(sizeof(DirectoryScanner)); bool safe = joined && realpath(joined, resolved) && strncmp(root, resolved, strlen(root)) == 0 &&
(resolved[strlen(root)] == '\0' || resolved[strlen(root)] == '/');
free(joined);
return safe;
}
typedef struct {
char* path;
struct stat stats;
bool is_directory;
} ScannerEntry;
/* Inspect symlinks, resolve the entry type, and apply file filters once for both scanners. */
static int scanner_inspect_entry(const ScannerOptions* options, const char* source_root,
const char* containing_dir, const char* name,
ScannerEntry* entry) {
entry->path = path_cat(containing_dir, name);
if (!entry->path)
return -1;
struct stat link_stats;
if (lstat(entry->path, &link_stats) != 0) {
free(entry->path);
return 0;
}
bool is_symlink = S_ISLNK(link_stats.st_mode);
if (is_symlink && !options->follow_symlinks && !options->copy_links && !options->safe_links &&
!options->copy_unsafe_links)
goto skip;
if (is_symlink && options->safe_links) {
char link_target[4096];
ssize_t length = readlink(entry->path, link_target, sizeof(link_target) - 1);
if (length < 0)
goto skip;
link_target[length] = '\0';
if (link_target[0] == '/' || !safe_relative_link(source_root, containing_dir, link_target))
goto skip;
}
if (is_symlink && options->copy_unsafe_links && !options->copy_links) {
char link_target[4096];
ssize_t length = readlink(entry->path, link_target, sizeof(link_target) - 1);
if (length < 0)
goto skip;
link_target[length] = '\0';
if (link_target[0] != '/')
goto skip;
}
if (is_symlink && options->follow_symlinks && !options->copy_links)
entry->stats = link_stats;
else if (stat(entry->path, &entry->stats) != 0)
goto skip;
entry->is_directory = S_ISDIR(entry->stats.st_mode);
if (entry->is_directory)
return 1;
for (int i = 0; i < options->exclude_count; i++)
if (glob_match(options->exclude_patterns[i], name))
goto skip;
if (options->include_count > 0) {
bool included = false;
for (int i = 0; i < options->include_count; i++)
if (glob_match(options->include_patterns[i], name))
included = true;
if (!included)
goto skip;
}
if ((options->max_size > 0 && (unsigned long long)entry->stats.st_size > options->max_size) ||
(options->min_size > 0 && (unsigned long long)entry->stats.st_size < options->min_size))
goto skip;
return 1;
skip:
free(entry->path);
entry->path = NULL;
return 0;
}
DirectoryScanner* directory_scanner_create_with_options(const char* root_directory,
const ScannerOptions* options) {
if (!root_directory || !options)
return NULL;
DirectoryScanner* scanner = calloc(1, sizeof(DirectoryScanner));
if (scanner == NULL) if (scanner == NULL)
return NULL; return NULL;
scanner->directories = queue_create(100, dir_entry_destroy); scanner->directories = queue_create(100, dir_entry_destroy);
if (!scanner->directories) {
free(scanner);
return NULL;
}
scanner->current_dir = NULL; scanner->current_dir = NULL;
scanner->current_path = NULL; scanner->current_path = NULL;
scanner->use_metadata = use_metadata; scanner->use_metadata = options->use_metadata;
scanner->chunk_size = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE; scanner->chunk_size = options->chunk_size > 0 ? options->chunk_size : DESIRED_CHUNK_SIZE;
scanner->exclude_patterns = exclude_patterns; scanner->exclude_patterns = options->exclude_patterns;
scanner->exclude_count = exclude_count; scanner->exclude_count = options->exclude_count;
scanner->include_patterns = include_patterns; scanner->include_patterns = options->include_patterns;
scanner->include_count = include_count; scanner->include_count = options->include_count;
scanner->max_size = max_size; scanner->max_size = options->max_size;
scanner->min_size = min_size; scanner->min_size = options->min_size;
scanner->max_depth = max_depth; scanner->max_depth = options->max_depth;
scanner->current_depth = 0; scanner->current_depth = 0;
scanner->follow_symlinks = follow_symlinks; scanner->follow_symlinks = options->follow_symlinks;
scanner->copy_links = copy_links; scanner->copy_links = options->copy_links;
scanner->safe_links = safe_links; scanner->safe_links = options->safe_links;
scanner->copy_unsafe_links = copy_unsafe_links; scanner->copy_unsafe_links = options->copy_unsafe_links;
scanner->checksum = checksum; scanner->checksum = options->checksum;
scanner->failed = false;
DirEntry* root = dir_entry_create(root_directory, 0); DirEntry* root = dir_entry_create(root_directory, 0);
if (!root) { if (!root) {
queue_destroy(scanner->directories); queue_destroy(scanner->directories);
@@ -81,6 +171,20 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_
return scanner; return scanner;
} }
DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_metadata,
unsigned long long chunk_size, char** exclude_patterns,
int exclude_count, char** include_patterns,
int include_count, unsigned long long max_size,
unsigned long long min_size, int max_depth,
bool follow_symlinks, bool copy_links, bool safe_links,
bool copy_unsafe_links, bool checksum) {
ScannerOptions options = {
use_metadata, chunk_size, exclude_patterns, exclude_count, include_patterns,
include_count, max_size, min_size, max_depth, 0,
follow_symlinks, copy_links, safe_links, copy_unsafe_links, checksum};
return directory_scanner_create_with_options(root_directory, &options);
}
void directory_scanner_destroy(DirectoryScanner* scanner) { void directory_scanner_destroy(DirectoryScanner* scanner) {
if (scanner == NULL) if (scanner == NULL)
return; return;
@@ -95,8 +199,12 @@ void directory_scanner_destroy(DirectoryScanner* scanner) {
static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) { static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
void** chunk_items = array_list_to_array(chunk_data); void** chunk_items = array_list_to_array(chunk_data);
if (!chunk_items)
return NULL;
Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size); Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size);
free(chunk_items); free(chunk_items);
if (!chunk)
return NULL;
chunk_data->item_destroyer = NULL; chunk_data->item_destroyer = NULL;
array_list_delete(chunk_data); array_list_delete(chunk_data);
return chunk; return chunk;
@@ -121,6 +229,7 @@ static int open_next_directory(DirectoryScanner* scanner) {
perror("Could not open directory"); perror("Could not open directory");
free(scanner->current_path); free(scanner->current_path);
scanner->current_path = NULL; scanner->current_path = NULL;
scanner->failed = true;
return -1; return -1;
} }
return 1; return 1;
@@ -128,6 +237,10 @@ static int open_next_directory(DirectoryScanner* scanner) {
Chunk* directory_scanner_next(DirectoryScanner* scanner) { Chunk* directory_scanner_next(DirectoryScanner* scanner) {
ArrayList* chunk_data = array_list_create(file_destroy); ArrayList* chunk_data = array_list_create(file_destroy);
if (!chunk_data) {
scanner->failed = true;
return NULL;
}
unsigned long long chunk_data_size = 0; unsigned long long chunk_data_size = 0;
while (1) { while (1) {
@@ -136,10 +249,10 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
if (ret == 0) if (ret == 0)
break; break;
if (ret < 0) if (ret < 0)
continue; break;
} }
struct dirent* entry = readdir(scanner->current_dir); const struct dirent* entry = readdir(scanner->current_dir);
if (entry == NULL) { if (entry == NULL) {
closedir(scanner->current_dir); closedir(scanner->current_dir);
scanner->current_dir = NULL; scanner->current_dir = NULL;
@@ -151,67 +264,34 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue; continue;
char* cur_path = path_cat(scanner->current_path, entry->d_name); ScannerOptions options = {scanner->use_metadata, scanner->chunk_size,
struct stat stats; scanner->exclude_patterns, scanner->exclude_count,
struct stat lstats; scanner->include_patterns, scanner->include_count,
bool is_symlink = false; scanner->max_size, scanner->min_size,
if (lstat(cur_path, &lstats) != 0) { scanner->max_depth, 0,
free(cur_path); scanner->follow_symlinks, scanner->copy_links,
scanner->safe_links, scanner->copy_unsafe_links,
scanner->checksum};
ScannerEntry inspected;
int inspection = scanner_inspect_entry(&options, scanner->current_path, scanner->current_path,
entry->d_name, &inspected);
if (inspection < 0) {
scanner->failed = true;
break;
}
if (inspection == 0)
continue; continue;
} char* cur_path = inspected.path;
is_symlink = S_ISLNK(lstats.st_mode); struct stat stats = inspected.stats;
if (is_symlink && !scanner->follow_symlinks && !scanner->copy_links && !scanner->safe_links && if (inspected.is_directory) {
!scanner->copy_unsafe_links) {
free(cur_path);
continue;
}
if (is_symlink && scanner->safe_links) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path);
continue;
}
link_target[len] = '\0';
if (link_target[0] == '/') {
free(cur_path);
continue;
}
}
if (is_symlink && scanner->copy_unsafe_links && !scanner->copy_links) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path);
continue;
}
link_target[len] = '\0';
bool unsafe = (link_target[0] == '/');
if (!unsafe) {
free(cur_path);
continue;
}
}
bool use_lstat = is_symlink && scanner->follow_symlinks && !scanner->copy_links;
if (use_lstat) {
stats = lstats;
} else {
if (stat(cur_path, &stats) != 0) {
free(cur_path);
continue;
}
}
if (S_ISDIR(stats.st_mode)) {
int next_depth = scanner->current_depth + 1; int next_depth = scanner->current_depth + 1;
if (scanner->max_depth <= 0 || next_depth < scanner->max_depth) { if (scanner->max_depth <= 0 || next_depth < scanner->max_depth) {
DirEntry* de = dir_entry_create(cur_path, next_depth); DirEntry* de = dir_entry_create(cur_path, next_depth);
if (!queue_enqueue(scanner->directories, de)) if (!de || !queue_enqueue(scanner->directories, de)) {
dir_entry_destroy(de); dir_entry_destroy(de);
scanner->failed = true;
}
} }
free(cur_path); free(cur_path);
} else { } else {
@@ -219,93 +299,88 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
free(cur_path); free(cur_path);
continue; continue;
} }
bool excluded = false;
for (int i = 0; i < scanner->exclude_count; i++) {
if (glob_match(scanner->exclude_patterns[i], entry->d_name)) {
excluded = true;
break;
}
}
if (excluded) {
free(cur_path);
continue;
}
if (scanner->include_count > 0) {
bool included = false;
for (int i = 0; i < scanner->include_count; i++) {
if (glob_match(scanner->include_patterns[i], entry->d_name)) {
included = true;
break;
}
}
if (!included) {
free(cur_path);
continue;
}
}
if ((scanner->max_size > 0 && (unsigned long long)stats.st_size > scanner->max_size) ||
(scanner->min_size > 0 && (unsigned long long)stats.st_size < scanner->min_size)) {
free(cur_path);
continue;
}
File* file = file_create(cur_path); File* file = file_create(cur_path);
if (file == NULL) { if (file == NULL) {
free(cur_path); free(cur_path);
scanner->failed = true;
continue; continue;
} }
file->data->size = stats.st_size; file->data->size = stats.st_size;
if (scanner->use_metadata) if (scanner->use_metadata)
file->metadata = file_metadata_create(&stats); file->metadata = file_metadata_create(&stats);
array_list_add(chunk_data, file); if (scanner->use_metadata && !file->metadata) {
file_destroy(file);
free(cur_path);
scanner->failed = true;
break;
}
if (!array_list_add(chunk_data, file)) {
file_destroy(file);
scanner->failed = true;
break;
}
chunk_data_size += file->data->size; chunk_data_size += file->data->size;
if (chunk_data_size > scanner->chunk_size) { if (chunk_data_size > scanner->chunk_size) {
free(cur_path); free(cur_path);
return chunk_data_to_chunk(chunk_data); Chunk* result = chunk_data_to_chunk(chunk_data);
if (!result)
scanner->failed = true;
return result;
} }
free(cur_path); free(cur_path);
} }
} }
if (chunk_data->size > 0) if (chunk_data->size > 0) {
return chunk_data_to_chunk(chunk_data); Chunk* result = chunk_data_to_chunk(chunk_data);
if (!result)
scanner->failed = true;
return result;
}
array_list_delete(chunk_data); array_list_delete(chunk_data);
return NULL; return NULL;
} }
bool directory_scanner_failed(const DirectoryScanner* scanner) {
return scanner == NULL || scanner->failed;
}
typedef struct { typedef struct {
ParallelScanner* ps; ParallelScanner* ps;
char** dirs; char** dirs;
int dir_count; int dir_count;
bool use_metadata; ScannerOptions options;
unsigned long long chunk_size;
char** exclude_patterns;
int exclude_count;
char** include_patterns;
int include_count;
unsigned long long max_size;
unsigned long long min_size;
int max_depth;
bool follow_symlinks;
bool copy_links;
bool safe_links;
bool copy_unsafe_links;
bool checksum;
} ParallelWorkerArg; } ParallelWorkerArg;
static int parallel_worker_thread(void* arg) { static int parallel_worker_thread(void* arg) {
ParallelWorkerArg* wa = (ParallelWorkerArg*)arg; ParallelWorkerArg* wa = (ParallelWorkerArg*)arg;
for (int i = 0; i < wa->dir_count; i++) { for (int i = 0; i < wa->dir_count; i++) {
DirectoryScanner* ds = directory_scanner_create( DirectoryScanner* ds = directory_scanner_create_with_options(wa->dirs[i], &wa->options);
wa->dirs[i], wa->use_metadata, wa->chunk_size, wa->exclude_patterns, wa->exclude_count, if (!ds) {
wa->include_patterns, wa->include_count, wa->max_size, wa->min_size, wa->max_depth, mtx_lock(&wa->ps->result_mutex);
wa->follow_symlinks, wa->copy_links, wa->safe_links, wa->copy_unsafe_links, wa->checksum); wa->ps->failed = true;
atomic_store(&wa->ps->cancelled, true);
cnd_broadcast(&wa->ps->result_not_empty);
cnd_broadcast(&wa->ps->result_not_full);
mtx_unlock(&wa->ps->result_mutex);
break;
}
Chunk* chunk; Chunk* chunk;
while ((chunk = directory_scanner_next(ds)) != NULL) { while ((chunk = directory_scanner_next(ds)) != NULL) {
queue_enqueue_multithreaded(wa->ps->result_queue, chunk, &wa->ps->result_mutex, if (!queue_enqueue_multithreaded_cancel(wa->ps->result_queue, chunk, &wa->ps->result_mutex,
&wa->ps->result_not_empty, &wa->ps->result_not_full); &wa->ps->result_not_empty, &wa->ps->result_not_full,
&wa->ps->cancelled)) {
chunk_destroy(chunk);
break;
}
}
if (directory_scanner_failed(ds)) {
mtx_lock(&wa->ps->result_mutex);
wa->ps->failed = true;
atomic_store(&wa->ps->cancelled, true);
cnd_broadcast(&wa->ps->result_not_empty);
cnd_broadcast(&wa->ps->result_not_full);
mtx_unlock(&wa->ps->result_mutex);
} }
directory_scanner_destroy(ds); directory_scanner_destroy(ds);
free(wa->dirs[i]); free(wa->dirs[i]);
@@ -315,7 +390,7 @@ static int parallel_worker_thread(void* arg) {
free(wa); free(wa);
mtx_lock(&ps->result_mutex); mtx_lock(&ps->result_mutex);
ps->completed++; ps->completed++;
if (ps->completed >= ps->num_threads) { if (ps->completed >= ps->expected_threads) {
ps->done = true; ps->done = true;
cnd_signal(&ps->result_not_empty); cnd_signal(&ps->result_not_empty);
} }
@@ -323,13 +398,10 @@ static int parallel_worker_thread(void* arg) {
return thrd_success; return thrd_success;
} }
ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata, ParallelScanner* parallel_scanner_create_with_options(const char* root_directory,
unsigned long long chunk_size, char** exclude_patterns, const ScannerOptions* options) {
int exclude_count, char** include_patterns, if (!root_directory || !options)
int include_count, unsigned long long max_size, return NULL;
unsigned long long min_size, int max_depth,
int num_threads, bool follow_symlinks, bool copy_links,
bool safe_links, bool copy_unsafe_links, bool checksum) {
ParallelScanner* ps = calloc(1, sizeof(ParallelScanner)); ParallelScanner* ps = calloc(1, sizeof(ParallelScanner));
if (!ps) if (!ps)
return NULL; return NULL;
@@ -338,6 +410,7 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata
free(ps); free(ps);
return NULL; return NULL;
} }
atomic_init(&ps->cancelled, false);
int init = 0; int init = 0;
bool ok = true; bool ok = true;
if (mtx_init(&ps->result_mutex, mtx_plain) != thrd_success) if (mtx_init(&ps->result_mutex, mtx_plain) != thrd_success)
@@ -374,137 +447,110 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata
ArrayList* root_files = array_list_create(file_destroy); ArrayList* root_files = array_list_create(file_destroy);
ArrayList* subdirs = array_list_create(free); ArrayList* subdirs = array_list_create(free);
struct dirent* entry; if (!root_files || !subdirs) {
array_list_delete(root_files);
array_list_delete(subdirs);
closedir(dir);
parallel_scanner_destroy(ps);
return NULL;
}
const struct dirent* entry;
while ((entry = readdir(dir)) != NULL) { while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue; continue;
char* cur_path = path_cat(root_directory, entry->d_name); ScannerEntry inspected;
if (!cur_path) int inspection =
continue; scanner_inspect_entry(options, root_directory, root_directory, entry->d_name, &inspected);
struct stat lstats; if (inspection < 0) {
if (lstat(cur_path, &lstats) != 0) { ps->failed = true;
free(cur_path);
continue; continue;
} }
bool is_symlink = S_ISLNK(lstats.st_mode); if (inspection == 0)
// Skip symlinks unless the user explicitly enabled following/copying them.
if (is_symlink && !follow_symlinks && !copy_links && !safe_links && !copy_unsafe_links) {
free(cur_path);
continue; continue;
} char* cur_path = inspected.path;
struct stat st = inspected.stats;
// --safe-links: reject symlinks pointing outside the source tree. if (inspected.is_directory) {
if (is_symlink && safe_links) { if (!array_list_add(subdirs, cur_path)) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path); free(cur_path);
continue; ps->failed = true;
} }
link_target[len] = 0;
if (link_target[0] == '/') {
free(cur_path);
continue;
}
}
// --copy-unsafe-links (without --copy-links): only copy absolute symlinks.
if (is_symlink && copy_unsafe_links && !copy_links) {
char link_target[4096];
ssize_t len = readlink(cur_path, link_target, sizeof(link_target) - 1);
if (len < 0) {
free(cur_path);
continue;
}
link_target[len] = 0;
bool unsafe = (link_target[0] == '/');
if (!unsafe) {
free(cur_path);
continue;
}
}
// Determine whether to use lstat or stat results for the entry.
struct stat st;
bool use_lstat_res = is_symlink && follow_symlinks && !copy_links;
if (use_lstat_res) {
st = lstats;
} else { } else {
if (stat(cur_path, &st) != 0) {
free(cur_path);
continue;
}
}
if (S_ISDIR(st.st_mode)) {
array_list_add(subdirs, cur_path);
} else {
bool excluded = false;
for (int i = 0; i < exclude_count; i++) {
if (glob_match(exclude_patterns[i], entry->d_name)) {
excluded = true;
break;
}
}
if (excluded) {
free(cur_path);
continue;
}
if (include_count > 0) {
bool included = false;
for (int i = 0; i < include_count; i++) {
if (glob_match(include_patterns[i], entry->d_name)) {
included = true;
break;
}
}
if (!included) {
free(cur_path);
continue;
}
}
if ((max_size > 0 && (unsigned long long)st.st_size > max_size) ||
(min_size > 0 && (unsigned long long)st.st_size < min_size)) {
free(cur_path);
continue;
}
File* file = file_create(cur_path); File* file = file_create(cur_path);
free(cur_path); free(cur_path);
if (!file) if (!file) {
ps->failed = true;
continue; continue;
}
file->data->size = st.st_size; file->data->size = st.st_size;
if (use_metadata) if (options->use_metadata)
file->metadata = file_metadata_create(&st); file->metadata = file_metadata_create(&st);
array_list_add(root_files, file); if (options->use_metadata && !file->metadata) {
file_destroy(file);
ps->failed = true;
continue;
}
if (!array_list_add(root_files, file)) {
file_destroy(file);
ps->failed = true;
}
} }
} }
closedir(dir); closedir(dir);
unsigned long long cs = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE; unsigned long long cs = options->chunk_size > 0 ? options->chunk_size : DESIRED_CHUNK_SIZE;
if (root_files->size > 0) { if (root_files->size > 0) {
ArrayList* batch = array_list_create(NULL); ArrayList* batch = array_list_create(NULL);
if (!batch) {
ps->failed = true;
array_list_delete(root_files);
array_list_delete(subdirs);
parallel_scanner_destroy(ps);
return NULL;
}
unsigned long long batch_size = 0; unsigned long long batch_size = 0;
Chunk* first = NULL; Chunk* first = NULL;
for (int i = 0; i < root_files->size; i++) { for (int i = 0; i < root_files->size; i++) {
File* f = (File*)root_files->items[i]; File* f = (File*)root_files->items[i];
array_list_add(batch, f); if (!array_list_add(batch, f)) {
ps->failed = true;
break;
}
batch_size += f->data->size; batch_size += f->data->size;
if (batch_size >= cs || i == root_files->size - 1) { if (batch_size >= cs || i == root_files->size - 1) {
void** items = array_list_to_array(batch); void** items = array_list_to_array(batch);
if (!items) {
ps->failed = true;
batch->item_destroyer = file_destroy;
array_list_delete(batch);
batch = NULL;
break;
}
Chunk* c = chunk_create((File**)items, batch->size); Chunk* c = chunk_create((File**)items, batch->size);
free(items); free(items);
if (!c) {
ps->failed = true;
batch->item_destroyer = file_destroy;
array_list_delete(batch);
batch = NULL;
break;
}
batch->item_destroyer = NULL; batch->item_destroyer = NULL;
array_list_delete(batch); array_list_delete(batch);
batch = NULL; batch = NULL;
if (!first) { if (!first) {
first = c; first = c;
} else { } else {
queue_enqueue_multithreaded(ps->result_queue, c, &ps->result_mutex, &ps->result_not_empty, if (!queue_enqueue(ps->result_queue, c)) {
&ps->result_not_full); chunk_destroy(c);
ps->failed = true;
}
} }
if (i < root_files->size - 1) { if (i < root_files->size - 1) {
batch = array_list_create(NULL); batch = array_list_create(NULL);
if (!batch) {
ps->failed = true;
break;
}
batch_size = 0; batch_size = 0;
} }
} }
@@ -518,12 +564,13 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata
} }
array_list_delete(root_files); array_list_delete(root_files);
int n = num_threads > 0 ? num_threads : 4; int n = options->num_threads > 0 ? options->num_threads : 4;
if (n > subdirs->size) if (n > subdirs->size)
n = subdirs->size > 0 ? subdirs->size : 1; n = subdirs->size > 0 ? subdirs->size : 1;
if (subdirs->size > 0) { if (subdirs->size > 0) {
ps->num_threads = n; ps->num_threads = n;
ps->expected_threads = n;
ps->threads = calloc(n, sizeof(thrd_t)); ps->threads = calloc(n, sizeof(thrd_t));
if (!ps->threads) { if (!ps->threads) {
array_list_delete(subdirs); array_list_delete(subdirs);
@@ -533,51 +580,77 @@ ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata
int dirs_per_thread = subdirs->size / n; int dirs_per_thread = subdirs->size / n;
int remainder = subdirs->size % n; int remainder = subdirs->size % n;
int start = 0; int start = 0;
ps->num_threads = 0;
for (int t = 0; t < n; t++) { for (int t = 0; t < n; t++) {
int count = dirs_per_thread + (t < remainder ? 1 : 0); int count = dirs_per_thread + (t < remainder ? 1 : 0);
if (count == 0) if (count == 0)
break; break;
ParallelWorkerArg* wa = calloc(1, sizeof(ParallelWorkerArg)); ParallelWorkerArg* wa = calloc(1, sizeof(ParallelWorkerArg));
if (!wa) if (!wa) {
ps->failed = true;
break; break;
}
wa->ps = ps; wa->ps = ps;
wa->dirs = calloc(count, sizeof(char*)); wa->dirs = calloc(count, sizeof(char*));
if (!wa->dirs) { if (!wa->dirs) {
free(wa); free(wa);
ps->failed = true;
break; break;
} }
for (int j = 0; j < count; j++) bool dup_ok = true;
for (int j = 0; j < count; j++) {
wa->dirs[j] = str_dup((char*)subdirs->items[start + j]); wa->dirs[j] = str_dup((char*)subdirs->items[start + j]);
if (!wa->dirs[j])
dup_ok = false;
}
if (!dup_ok) {
for (int j = 0; j < count; j++)
free(wa->dirs[j]);
free(wa->dirs);
free(wa);
ps->failed = true;
break;
}
wa->dir_count = count; wa->dir_count = count;
wa->use_metadata = use_metadata; wa->options = *options;
wa->chunk_size = cs; wa->options.chunk_size = cs;
wa->exclude_patterns = exclude_patterns;
wa->exclude_count = exclude_count;
wa->include_patterns = include_patterns;
wa->include_count = include_count;
wa->max_size = max_size;
wa->min_size = min_size;
wa->max_depth = max_depth;
wa->follow_symlinks = follow_symlinks;
wa->copy_links = copy_links;
wa->safe_links = safe_links;
wa->copy_unsafe_links = copy_unsafe_links;
wa->checksum = checksum;
start += count; start += count;
if (thrd_create(&ps->threads[t], parallel_worker_thread, wa) != thrd_success) { if (thrd_create(&ps->threads[t], parallel_worker_thread, wa) != thrd_success) {
for (int j = 0; j < count; j++) for (int j = 0; j < count; j++)
free(wa->dirs[j]); free(wa->dirs[j]);
free(wa->dirs); free(wa->dirs);
free(wa); free(wa);
ps->num_threads = t; ps->failed = true;
atomic_store(&ps->cancelled, true);
ps->expected_threads = ps->created_threads;
mtx_lock(&ps->result_mutex);
cnd_broadcast(&ps->result_not_empty);
cnd_broadcast(&ps->result_not_full);
mtx_unlock(&ps->result_mutex);
break; break;
} }
ps->num_threads++;
ps->created_threads++;
} }
} }
array_list_delete(subdirs); array_list_delete(subdirs);
return ps; return ps;
} }
ParallelScanner* parallel_scanner_create(const char* root_directory, bool use_metadata,
unsigned long long chunk_size, char** exclude_patterns,
int exclude_count, char** include_patterns,
int include_count, unsigned long long max_size,
unsigned long long min_size, int max_depth,
int num_threads, bool follow_symlinks, bool copy_links,
bool safe_links, bool copy_unsafe_links, bool checksum) {
ScannerOptions options = {use_metadata, chunk_size, exclude_patterns, exclude_count,
include_patterns, include_count, max_size, min_size,
max_depth, num_threads, follow_symlinks, copy_links,
safe_links, copy_unsafe_links, checksum};
return parallel_scanner_create_with_options(root_directory, &options);
}
Chunk* parallel_scanner_next(ParallelScanner* ps) { Chunk* parallel_scanner_next(ParallelScanner* ps) {
if (ps->initial_chunk) { if (ps->initial_chunk) {
Chunk* c = ps->initial_chunk; Chunk* c = ps->initial_chunk;
@@ -585,7 +658,9 @@ Chunk* parallel_scanner_next(ParallelScanner* ps) {
return c; return c;
} }
if (ps->num_threads == 0) { if (ps->num_threads == 0) {
mtx_lock(&ps->result_mutex);
ps->done = true; ps->done = true;
mtx_unlock(&ps->result_mutex);
return NULL; return NULL;
} }
Chunk* chunk = queue_dequeue_multithreaded( Chunk* chunk = queue_dequeue_multithreaded(
@@ -593,12 +668,19 @@ Chunk* parallel_scanner_next(ParallelScanner* ps) {
return chunk; return chunk;
} }
bool parallel_scanner_failed(const ParallelScanner* ps) {
return ps == NULL || ps->failed;
}
void parallel_scanner_destroy(ParallelScanner* ps) { void parallel_scanner_destroy(ParallelScanner* ps) {
if (!ps) if (!ps)
return; return;
mtx_lock(&ps->result_mutex);
ps->done = true; ps->done = true;
cnd_signal(&ps->result_not_empty); atomic_store(&ps->cancelled, true);
cnd_broadcast(&ps->result_not_empty);
cnd_broadcast(&ps->result_not_full); cnd_broadcast(&ps->result_not_full);
mtx_unlock(&ps->result_mutex);
for (int i = 0; i < ps->num_threads; i++) for (int i = 0; i < ps->num_threads; i++)
thrd_join(ps->threads[i], NULL); thrd_join(ps->threads[i], NULL);
free(ps->threads); free(ps->threads);
+31 -1
View File
@@ -6,6 +6,25 @@
#include <dirent.h> #include <dirent.h>
#include <stdbool.h> #include <stdbool.h>
#include <threads.h> #include <threads.h>
#include <stdatomic.h>
typedef struct {
bool use_metadata;
unsigned long long chunk_size;
char** exclude_patterns;
int exclude_count;
char** include_patterns;
int include_count;
unsigned long long max_size;
unsigned long long min_size;
int max_depth;
int num_threads;
bool follow_symlinks;
bool copy_links;
bool safe_links;
bool copy_unsafe_links;
bool checksum;
} ScannerOptions;
typedef struct { typedef struct {
Queue* directories; Queue* directories;
@@ -26,6 +45,7 @@ typedef struct {
bool safe_links; bool safe_links;
bool copy_unsafe_links; bool copy_unsafe_links;
bool checksum; bool checksum;
bool failed;
} DirectoryScanner; } DirectoryScanner;
typedef struct { typedef struct {
@@ -34,8 +54,12 @@ typedef struct {
cnd_t result_not_empty; cnd_t result_not_empty;
cnd_t result_not_full; cnd_t result_not_full;
int num_threads; int num_threads;
int expected_threads;
int created_threads;
thrd_t* threads; thrd_t* threads;
bool done; bool done;
bool failed;
atomic_bool cancelled;
int completed; int completed;
Chunk* initial_chunk; Chunk* initial_chunk;
} ParallelScanner; } ParallelScanner;
@@ -47,17 +71,23 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_
unsigned long long min_size, int max_depth, unsigned long long min_size, int max_depth,
bool follow_symlinks, bool copy_links, bool safe_links, bool follow_symlinks, bool copy_links, bool safe_links,
bool copy_unsafe_links, bool checksum); bool copy_unsafe_links, bool checksum);
DirectoryScanner* directory_scanner_create_with_options(const char* root_directory,
const ScannerOptions* options);
Chunk* directory_scanner_next(DirectoryScanner* scanner); Chunk* directory_scanner_next(DirectoryScanner* scanner);
bool directory_scanner_failed(const DirectoryScanner* scanner);
void directory_scanner_destroy(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner);
ParallelScanner* parallel_scanner_create(char* root_directory, bool use_metadata, ParallelScanner* parallel_scanner_create(const char* root_directory, bool use_metadata,
unsigned long long chunk_size, char** exclude_patterns, unsigned long long chunk_size, char** exclude_patterns,
int exclude_count, char** include_patterns, int exclude_count, char** include_patterns,
int include_count, unsigned long long max_size, int include_count, unsigned long long max_size,
unsigned long long min_size, int max_depth, unsigned long long min_size, int max_depth,
int num_threads, bool follow_symlinks, bool copy_links, int num_threads, bool follow_symlinks, bool copy_links,
bool safe_links, bool copy_unsafe_links, bool checksum); bool safe_links, bool copy_unsafe_links, bool checksum);
ParallelScanner* parallel_scanner_create_with_options(const char* root_directory,
const ScannerOptions* options);
Chunk* parallel_scanner_next(ParallelScanner* scanner); Chunk* parallel_scanner_next(ParallelScanner* scanner);
bool parallel_scanner_failed(const ParallelScanner* scanner);
void parallel_scanner_destroy(ParallelScanner* scanner); void parallel_scanner_destroy(ParallelScanner* scanner);
#endif #endif
+74
View File
@@ -0,0 +1,74 @@
#include "usage.h"
#include <stdio.h>
#include <delta.h>
#include <chunk.h>
void print_usage(void) {
printf("Usage:\n");
printf(" fastsync [options] <source> <destination>\n");
printf(" fastsync [options] --source-dir <src> --dest-dir <dst>\n");
printf("\n");
printf("Destination formats:\n");
printf(" user@host:/path SSH transport (rsync-style)\n");
printf(" host:/path SSH transport (current user)\n");
printf(" /local/path TCP transport (requires server on localhost:8080)\n");
printf("\n");
printf("Options:\n");
printf(" -c [level] Enable compression (level 1-22, default 5)\n");
printf(" -z [level] Alias for -c\n");
printf(" -a, --archive Archive mode (-c -m -M)\n");
printf(" -n, --dry-run Show what would be transferred\n");
printf(" -p <port> SSH port (default: 22)\n");
printf(" --progress Show transfer progress\n");
printf(" --delete Delete files on receiver not in source\n");
printf(" --exclude <pattern> Exclude files matching pattern\n");
printf(" --include <pattern> Only include files matching pattern\n");
printf(" --exclude-from <file> Read exclude patterns from file\n");
printf(" --include-from <file> Read include patterns from file\n");
printf(" --max-size <n> Skip files larger than n bytes\n");
printf(" --min-size <n> Skip files smaller than n bytes\n");
printf(" --incremental Skip files unchanged since last transfer\n");
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
DELTA_BLOCK_SIZE_DEFAULT);
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
DELTA_MAX_FILE_SIZE);
printf(" -m Enable multithreading\n");
printf(" -s Enable chunk serialization\n");
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
printf(" -v, --verbose Enable debug logging\n");
printf(" -M, --preserve Preserve file metadata\n");
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
printf(" --source-dir <path> Source directory\n");
printf(" --dest-dir <path> Destination directory\n");
printf(" --save-to-disk Write received files to disk\n");
printf(" --server-host <ip> Server IP address (default: 127.0.0.1)\n");
printf(" --server-port <n> Server port (default: 8080)\n");
printf(" --bwlimit <KB/s> Bandwidth limit in kilobytes per second\n");
printf(" --tls Enable TLS encryption\n");
printf(" --cert <path> TLS certificate file (PEM)\n");
printf(" --key <path> TLS private key file (PEM)\n");
printf(" --ca <path> TLS CA certificate file (PEM)\n");
printf(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
printf(" -T <sec> Alias for --timeout\n");
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\n");
printf(" --backup Backup existing files before overwriting\n");
printf(" --backup-dir <dir> Directory for backups (requires --backup)\n");
printf(" --suffix <str> Backup suffix (default: ~)\n");
printf(" --stats Print transfer statistics at end\n");
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
printf(" --log-file <path> Write log messages to file\n");
printf(" --partial Keep partial files on interrupted transfer\n");
printf(" --partial-dir <dir> Directory for partial files\n");
printf(" --fastsync-server-path <path>\n");
printf(" Path to fastsync-server on remote (default: fastsync-server)\n");
printf(" -l, --links Copy symlinks as symlinks\n");
printf(" --copy-links Transform symlinks into referent files\n");
printf(" --safe-links Skip symlinks that point outside transfer tree\n");
printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n");
printf(" -S, --sparse Handle sparse files efficiently\n");
printf(" --inplace Update files in-place (no temp+rename)\n");
printf(" --compress-level <n> Compression level (default: 5)\n");
printf(" --help Show this help\n");
printf(" -V, --version Show version\n");
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef USAGE_H
#define USAGE_H
void print_usage(void);
#endif
+126
View File
@@ -0,0 +1,126 @@
#include "receiver.h"
#include "chunk.h"
#include "log.h"
#include "protocol.h"
#include "utils.h"
#include <stdlib.h>
#include <sys/stat.h>
static bool receiver_process_chunk(Chunk* chunk, const ReceiverSink* sink) {
for (int i = 0; i < chunk->element_count; i++) {
File* file = chunk->items[i];
chunk->items[i] = NULL;
if (!sink->store_file(file, sink->context)) {
chunk_destroy(chunk);
return false;
}
}
chunk_destroy(chunk);
return true;
}
static bool receiver_process_batch(Config* config, int file_descriptor) {
int count;
if (config->checksum || !receive_int(file_descriptor, &count) || count < 0 ||
count > MAX_MANIFEST_ENTRIES)
return false;
for (int i = 0; i < count; i++) {
char* check_path = receive_str(file_descriptor);
if (!check_path)
return false;
unsigned long long check_size;
long long check_mtime;
if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) ||
!receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime))) {
free(check_path);
return false;
}
if (!utils_valid_batch_path(check_path)) {
free(check_path);
send_status(file_descriptor, STATUS_ERROR);
return false;
}
char* full_path = path_cat(config->receive_root_directory, check_path);
struct stat st;
bool has_old = full_path && lstat(full_path, &st) == 0;
bool match = has_old && (unsigned long long)st.st_size == check_size &&
(long long)st.st_mtime == check_mtime;
bool sent = send_status(file_descriptor, match ? STATUS_OK : STATUS_NEXT);
free(full_path);
free(check_path);
if (!sent)
return false;
}
return true;
}
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
Status status;
if (!receive_status(file_descriptor, &status))
return -1;
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
if (status == STATUS_KEEPALIVE) {
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
return -1;
goto next;
}
if (status == STATUS_ABORT) {
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
return -1;
}
if (status == STATUS_CHECK) {
bool skipped;
File* file = receive_incremental_check(file_descriptor, config, &skipped);
if (!skipped && (!file || !sink->store_file(file, sink->context)))
goto receive_error;
} else if (status == STATUS_CHUNK) {
Chunk* chunk = receive_chunk_data(file_descriptor, config);
if (!chunk || !receiver_process_chunk(chunk, sink))
goto receive_error;
} else if (status == STATUS_CHECK_BATCH) {
if (!receiver_process_batch(config, file_descriptor))
return -1;
goto next;
} else {
File* file = file_receive(config, file_descriptor);
if (!file) {
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
goto receive_error;
}
if (!sink->store_file(file, sink->context))
goto receive_error;
}
next:
if (!receive_status(file_descriptor, &status))
goto receive_error;
}
if (status == STATUS_MANIFEST && receive_manifest(file_descriptor, config, &status) != 0)
return -1;
if (status != STATUS_FINISHED) {
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
goto receive_error;
}
if (sink->send_success && !send_status(file_descriptor, STATUS_OK))
return -1;
return 0;
receive_error:
if (sink->send_error)
send_status(file_descriptor, STATUS_ERROR);
return -1;
}
static bool receiver_save_file(File* file, void* context) {
Config* config = context;
bool success =
!config->save_to_disk || file_save_to_disk(config->receive_root_directory, file, config);
file_destroy(file);
return success;
}
int receiver_receive_files(Config* config, int file_descriptor) {
ReceiverSink sink = {receiver_save_file, config, true, true};
return receiver_process(config, file_descriptor, &sink);
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef RECEIVER_H
#define RECEIVER_H
#include "config.h"
#include "file.h"
typedef bool (*ReceiverFileSink)(File* file, void* context);
typedef struct {
ReceiverFileSink store_file;
void* context;
bool send_error;
bool send_success;
} ReceiverSink;
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink);
int receiver_receive_files(Config* config, int file_descriptor);
#endif
+108 -118
View File
@@ -1,129 +1,100 @@
#include "array_list.h"
#include "chunk.h"
#include "config.h" #include "config.h"
#include "data.h"
#include "file.h" #include "file.h"
#include "log.h" #include "log.h"
#include "multiprocessing.h" #include "multiprocessing.h"
#include "protocol.h"
#include "queue.h" #include "queue.h"
#include "receiver.h"
#include "transport_tcp.h" #include "transport_tcp.h"
#include "transport_tls.h" #include "transport_tls.h"
#include "unistd.h" #include "unistd.h"
#include "utils.h" #include "utils.h"
#include <fcntl.h>
#include <limits.h>
#include <signal.h> #include <signal.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h>
int receive_files(Config* config, int fd) { static char* authorized_root;
Status status; static int authorized_root_fd = -1;
if (!receive_status(fd, &status)) static bool allow_delete;
return -1;
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || static bool path_is_within(const char* root, const char* path) {
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) { size_t n = strlen(root);
if (status == STATUS_KEEPALIVE) { return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/');
send_status(fd, STATUS_KEEPALIVE); }
goto next;
}
if (status == STATUS_ABORT) {
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
return -1;
}
if (status == STATUS_CHECK) {
bool skipped;
File* file = receive_incremental_check(fd, config, &skipped);
if (skipped)
goto next;
if (file == NULL && !skipped)
return -1;
if (config->save_to_disk)
file_save_to_disk(config->receive_root_directory, file, NULL);
file_destroy(file);
} else if (status == STATUS_CHUNK) {
Chunk* chunk = receive_chunk_data(fd, config);
if (chunk == NULL) {
send_status(fd, STATUS_ERROR);
return -1;
}
for (int i = 0; i < chunk->element_count; i++) {
if (config->save_to_disk)
file_save_to_disk(config->receive_root_directory, chunk->items[i], NULL);
}
chunk_destroy(chunk);
} else if (status == STATUS_CHECK_BATCH) {
int count;
if (!receive_int(fd, &count))
return -1;
for (int i = 0; i < count; i++) {
char* check_path = receive_str(fd);
if (!check_path)
return -1;
unsigned long long check_size;
long long check_mtime;
if (!receive_n_data(fd, &check_size, sizeof(check_size)) ||
!receive_n_data(fd, &check_mtime, sizeof(check_mtime))) {
free(check_path);
return -1;
}
char* full_path = path_cat(config->receive_root_directory, check_path);
struct stat st;
bool has_old = full_path && lstat(full_path, &st) == 0;
bool match = has_old && (unsigned long long)st.st_size == check_size &&
(long long)st.st_mtime == check_mtime;
if (match)
send_status(fd, STATUS_OK);
else
send_status(fd, STATUS_NEXT);
free(full_path);
free(check_path);
}
goto next;
} else {
File* file = file_receive(config, fd);
if (file == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
send_status(fd, STATUS_ERROR);
return -1;
}
if (config->save_to_disk)
file_save_to_disk(config->receive_root_directory, file, NULL);
file_destroy(file);
}
next:
if (!receive_status(fd, &status)) {
send_status(fd, STATUS_ERROR);
return -1;
}
}
if (status == STATUS_MANIFEST) { static bool __attribute__((unused)) configure_authorization(const char* root) {
if (receive_manifest(fd, config, &status) != 0) char resolved[PATH_MAX];
return -1; if (!root || !realpath(root, resolved))
return false;
authorized_root = str_dup(resolved);
if (!authorized_root)
return false;
authorized_root_fd = open(resolved, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (authorized_root_fd < 0) {
free(authorized_root);
authorized_root = NULL;
return false;
} }
if (status != STATUS_FINISHED) { file_set_authorized_root(authorized_root_fd, authorized_root);
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status"); utils_set_authorized_root_fd(authorized_root_fd);
send_status(fd, STATUS_ERROR); return true;
return -1;
}
send_status(fd, STATUS_OK);
return 0;
} }
void handler(int file_descriptor) { void handler(int file_descriptor) {
SSL* ssl = io_get_ssl(); SSL* ssl = io_get_ssl();
ProtocolSession session;
protocol_session_init(&session, file_descriptor, file_descriptor);
protocol_session_set_ssl(&session, ssl);
protocol_session_bind(&session);
Config* config = config_receive(file_descriptor); Config* config = config_receive(file_descriptor);
if (config == NULL) { if (config == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to receive config"); log_message(LOG_LEVEL_ERROR, "Failed to receive config");
close(file_descriptor); close(file_descriptor);
protocol_session_unbind();
return; return;
} }
if (!authorized_root) {
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
config_delete(config);
close(file_descriptor);
protocol_session_unbind();
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);
protocol_session_unbind();
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);
protocol_session_unbind();
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) {
config_delete(config); config_delete(config);
close(file_descriptor); close(file_descriptor);
protocol_session_unbind();
return; return;
} }
PipelineContextReceiver* context = PipelineContextReceiver* context =
@@ -132,19 +103,19 @@ void handler(int file_descriptor) {
queue_destroy(q); queue_destroy(q);
config_delete(config); config_delete(config);
close(file_descriptor); close(file_descriptor);
protocol_session_unbind();
return; return;
} }
thrd_t receiver, writer; thrd_t receiver, writer;
bool receiver_created = false; bool receiver_created = thrd_create(&receiver, receive_thread, context) == thrd_success;
bool writer_created = false; bool writer_created = false;
receiver_created = (thrd_create(&receiver, receive_thread, context) == thrd_success);
if (receiver_created) if (receiver_created)
writer_created = (thrd_create(&writer, write_thread, context) == thrd_success); writer_created = thrd_create(&writer, write_thread, context) == thrd_success;
if (!receiver_created || !writer_created) { if (!receiver_created || !writer_created) {
perror("Error creating Threads"); perror("Error creating Threads");
if (receiver_created) { if (receiver_created) {
mtx_lock(&context->mutex); mtx_lock(&context->mutex);
context->cancelled = true; atomic_store(&context->cancelled, true);
cnd_broadcast(&context->condition_not_full); cnd_broadcast(&context->condition_not_full);
cnd_broadcast(&context->condition_not_empty); cnd_broadcast(&context->condition_not_empty);
mtx_unlock(&context->mutex); mtx_unlock(&context->mutex);
@@ -156,16 +127,23 @@ void handler(int file_descriptor) {
if (writer_created) if (writer_created)
thrd_join(writer, NULL); thrd_join(writer, NULL);
pipeline_context_receiver_destroy(context); pipeline_context_receiver_destroy(context);
protocol_session_unbind();
return; return;
} }
thrd_join(receiver, NULL); int receiver_result;
thrd_join(writer, NULL); int writer_result;
send_status(file_descriptor, STATUS_OK); thrd_join(receiver, &receiver_result);
thrd_join(writer, &writer_result);
send_status(file_descriptor, receiver_result == thrd_success && writer_result == thrd_success
? STATUS_OK
: STATUS_ERROR);
pipeline_context_receiver_destroy(context); pipeline_context_receiver_destroy(context);
} else { } else {
receive_files(config, file_descriptor); if (receiver_receive_files(config, file_descriptor) != 0)
log_message(LOG_LEVEL_ERROR, "Transfer failed");
config_delete(config); config_delete(config);
} }
protocol_session_unbind();
close(file_descriptor); close(file_descriptor);
} }
@@ -174,16 +152,14 @@ static Server* g_server = NULL;
static void cleanup(int sig) { static void cleanup(int sig) {
(void)sig; (void)sig;
if (g_server) { if (g_server)
server_delete(&g_server); server_delete(&g_server);
}
_exit(0); _exit(0);
} }
static void print_server_usage(void) { static void print_server_usage(void) {
printf("FastSync Server\n"); printf("FastSync Server\n");
printf("Usage: fastsync-server [options]\n"); printf("Usage: fastsync-server [options]\n\n");
printf("\n");
printf("Options:\n"); printf("Options:\n");
printf(" --stdio Run in stdio mode (SSH transport)\n"); printf(" --stdio Run in stdio mode (SSH transport)\n");
printf(" -p <port> TCP port (default: 8080, range: 1-65535)\n"); printf(" -p <port> TCP port (default: 8080, range: 1-65535)\n");
@@ -191,16 +167,18 @@ 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");
} }
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
bool use_tls = false; bool use_tls = false;
char* tls_cert = NULL; char *tls_cert = NULL, *tls_key = NULL, *tls_ca = NULL;
char* tls_key = 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++) {
@@ -208,9 +186,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) {
@@ -221,6 +197,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);
@@ -235,15 +215,25 @@ int main(int argc, char* argv[]) {
return 1; return 1;
} }
} }
if (tls_ca && !use_tls)
if (tls_ca && !use_tls) {
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls"); log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
}
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);
file_set_authorized_root(-1, NULL);
utils_set_authorized_root_fd(-1);
close(authorized_root_fd);
free(authorized_root);
return 0;
}
g_server = server_create(port); g_server = server_create(port);
if (g_server == NULL) { if (!g_server) {
log_message(LOG_LEVEL_ERROR, "Failed to create server"); log_message(LOG_LEVEL_ERROR, "Failed to create server");
return 1; return 1;
} }
@@ -265,4 +255,4 @@ int main(int argc, char* argv[]) {
} }
return 0; return 0;
} }
#endif /* !FASTSYNC_SERVER_AS_LIB */ #endif
+167 -249
View File
@@ -173,102 +173,157 @@ void config_delete(Config* config) {
free(config); free(config);
} }
/* Wire format order (must match config_receive and be updated when PROTOCOL_VERSION bumps): /* Each helper is deliberately ordered to match the wire format. Keep the
* version, send_directory, receive_root_directory, save_to_disk, use_multithreading, * helper call order in config_send and config_receive unchanged when adding
* use_chunk_serialization, use_compression, use_metadata, compression_level, chunk_size, * fields. */
* use_sendfile, use_delete, use_incremental, use_delta, delta_block_size, delta_max_file_size, static bool send_core_fields(int fd, const Config* c) {
* backup, backup_dir, follow_symlinks, copy_links, safe_links, copy_unsafe_links, return send_str(fd, c->version) && send_str(fd, c->send_directory) &&
* preserve_hard_links, preserve_acls, preserve_xattrs, preserve_devices, preserve_sparse, send_str(fd, c->receive_root_directory) && send_int(fd, c->save_to_disk) &&
* update, inplace, append, append_verify, delete_excluded, delete_after, max_delete, relative, send_int(fd, c->use_multithreading) && send_int(fd, c->use_chunk_serialization) &&
* prune_empty_dirs, temp_dir, partial, partial_dir, suffix, delete_before, checksum, send_int(fd, c->use_compression) && send_int(fd, c->use_metadata) &&
* compress_choice, status send_int(fd, c->compression_level) &&
*/ send_n_data(fd, &c->chunk_size, sizeof(c->chunk_size)) && send_int(fd, c->use_sendfile);
}
static bool send_delta_fields(int fd, const Config* c) {
return send_int(fd, c->use_delete) && send_int(fd, c->use_incremental) &&
send_int(fd, c->use_delta) &&
send_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) &&
send_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long));
}
static bool send_file_options(int fd, const Config* c) {
return send_int(fd, c->backup) && send_str(fd, c->backup_dir ? c->backup_dir : "") &&
send_int(fd, c->follow_symlinks) && send_int(fd, c->copy_links) &&
send_int(fd, c->safe_links) && send_int(fd, c->copy_unsafe_links) &&
send_int(fd, c->preserve_hard_links) && send_int(fd, c->preserve_acls) &&
send_int(fd, c->preserve_xattrs) && send_int(fd, c->preserve_devices) &&
send_int(fd, c->preserve_sparse);
}
static bool send_selection_options(int fd, const Config* c) {
return send_int(fd, c->update) && send_int(fd, c->inplace) && send_int(fd, c->append) &&
send_int(fd, c->append_verify) && send_int(fd, c->delete_excluded) &&
send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) &&
send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs);
}
static bool send_resume_options(int fd, const Config* c) {
return send_str(fd, c->temp_dir ? c->temp_dir : "") && send_int(fd, c->partial) &&
send_str(fd, c->partial_dir ? c->partial_dir : "") &&
send_str(fd, c->suffix ? c->suffix : "") && send_int(fd, c->delete_before) &&
send_int(fd, c->checksum) && send_str(fd, c->compress_choice ? c->compress_choice : "");
}
static bool receive_core_fields(int fd, Config* c) {
int value;
c->send_directory = receive_str(fd);
c->receive_root_directory = receive_str(fd);
if (!c->send_directory || !c->receive_root_directory)
return false;
if (!receive_int(fd, &value))
return false;
c->save_to_disk = value;
if (!receive_int(fd, &value))
return false;
c->use_multithreading = value;
if (!receive_int(fd, &value))
return false;
c->use_chunk_serialization = value;
if (!receive_int(fd, &value))
return false;
c->use_compression = value;
if (!receive_int(fd, &value))
return false;
c->use_metadata = value;
if (!receive_int(fd, &value))
return false;
c->compression_level = value;
if (!receive_n_data(fd, &c->chunk_size, sizeof(c->chunk_size)))
return false;
if (!receive_int(fd, &value))
return false;
c->use_sendfile = value;
return true;
}
static bool receive_delta_fields(int fd, Config* c) {
int value;
if (!receive_int(fd, &value))
return false;
c->use_delete = value;
if (!receive_int(fd, &value))
return false;
c->use_incremental = value;
if (!receive_int(fd, &value))
return false;
c->use_delta = value;
return receive_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) &&
receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long));
}
static bool receive_file_options(int fd, Config* c) {
int value;
if (!receive_int(fd, &value))
return false;
c->backup = value;
c->backup_dir = receive_str(fd);
if (!c->backup_dir)
return false;
bool* flags[] = {&c->follow_symlinks, &c->copy_links, &c->safe_links,
&c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls,
&c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse};
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
if (!receive_int(fd, &value))
return false;
*flags[i] = value;
}
return true;
}
static bool receive_selection_options(int fd, Config* c) {
int value;
bool* flags[] = {&c->update, &c->inplace, &c->append,
&c->append_verify, &c->delete_excluded, &c->delete_after};
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
if (!receive_int(fd, &value))
return false;
*flags[i] = value;
}
if (!receive_n_data(fd, &c->max_delete, sizeof(c->max_delete)))
return false;
if (!receive_int(fd, &value))
return false;
c->relative = value;
if (!receive_int(fd, &value))
return false;
c->prune_empty_dirs = value;
return true;
}
static bool receive_resume_options(int fd, Config* c) {
int value;
c->temp_dir = receive_str(fd);
if (!c->temp_dir || !receive_int(fd, &value))
return false;
c->partial = value;
c->partial_dir = receive_str(fd);
c->suffix = c->partial_dir ? receive_str(fd) : NULL;
if (!c->partial_dir || !c->suffix || !receive_int(fd, &value))
return false;
c->delete_before = value;
if (!receive_int(fd, &value))
return false;
c->checksum = value;
c->compress_choice = receive_str(fd);
return c->compress_choice != NULL;
}
bool config_send(int file_descriptor, const Config* config) { bool config_send(int file_descriptor, const Config* config) {
if (!send_str(file_descriptor, config->version)) if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) ||
return false; !send_file_options(file_descriptor, config) ||
if (!send_str(file_descriptor, config->send_directory)) !send_selection_options(file_descriptor, config) ||
return false; !send_resume_options(file_descriptor, config))
if (!send_str(file_descriptor, config->receive_root_directory))
return false;
if (!send_int(file_descriptor, config->save_to_disk))
return false;
if (!send_int(file_descriptor, config->use_multithreading))
return false;
if (!send_int(file_descriptor, config->use_chunk_serialization))
return false;
if (!send_int(file_descriptor, config->use_compression))
return false;
if (!send_int(file_descriptor, config->use_metadata))
return false;
if (!send_int(file_descriptor, config->compression_level))
return false;
if (!send_n_data(file_descriptor, &config->chunk_size, sizeof(config->chunk_size)))
return false;
if (!send_int(file_descriptor, config->use_sendfile))
return false;
if (!send_int(file_descriptor, config->use_delete))
return false;
if (!send_int(file_descriptor, config->use_incremental))
return false;
if (!send_int(file_descriptor, config->use_delta))
return false;
if (!send_n_data(file_descriptor, &config->delta_block_size, sizeof(config->delta_block_size)))
return false;
if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long)))
return false;
if (!send_int(file_descriptor, config->backup))
return false;
if (!send_str(file_descriptor, config->backup_dir ? config->backup_dir : ""))
return false;
if (!send_int(file_descriptor, config->follow_symlinks))
return false;
if (!send_int(file_descriptor, config->copy_links))
return false;
if (!send_int(file_descriptor, config->safe_links))
return false;
if (!send_int(file_descriptor, config->copy_unsafe_links))
return false;
if (!send_int(file_descriptor, config->preserve_hard_links))
return false;
if (!send_int(file_descriptor, config->preserve_acls))
return false;
if (!send_int(file_descriptor, config->preserve_xattrs))
return false;
if (!send_int(file_descriptor, config->preserve_devices))
return false;
if (!send_int(file_descriptor, config->preserve_sparse))
return false;
if (!send_int(file_descriptor, config->update))
return false;
if (!send_int(file_descriptor, config->inplace))
return false;
if (!send_int(file_descriptor, config->append))
return false;
if (!send_int(file_descriptor, config->append_verify))
return false;
if (!send_int(file_descriptor, config->delete_excluded))
return false;
if (!send_int(file_descriptor, config->delete_after))
return false;
if (!send_n_data(file_descriptor, &config->max_delete, sizeof(config->max_delete)))
return false;
if (!send_int(file_descriptor, config->relative))
return false;
if (!send_int(file_descriptor, config->prune_empty_dirs))
return false;
if (!send_str(file_descriptor, config->temp_dir ? config->temp_dir : ""))
return false;
if (!send_int(file_descriptor, config->partial))
return false;
if (!send_str(file_descriptor, config->partial_dir ? config->partial_dir : ""))
return false;
if (!send_str(file_descriptor, config->suffix ? config->suffix : ""))
return false;
if (!send_int(file_descriptor, config->delete_before))
return false;
if (!send_int(file_descriptor, config->checksum))
return false;
if (!send_str(file_descriptor, config->compress_choice ? config->compress_choice : ""))
return false; return false;
Status status; Status status;
if (!receive_status(file_descriptor, &status)) if (!receive_status(file_descriptor, &status))
@@ -280,174 +335,37 @@ bool config_send(int file_descriptor, const Config* config) {
return true; return true;
} }
/* Wire format order: see the comment above config_send. */
Config* config_receive(int file_descriptor) { Config* config_receive(int file_descriptor) {
Config* config = (Config*)malloc(sizeof(Config)); Config* config = config_create();
if (config == NULL) if (!config)
return NULL; return NULL;
config_set_defaults(config);
free(config->version); free(config->version);
config->version = receive_str(file_descriptor); config->version = receive_str(file_descriptor);
if (!config->version) { if (!config->version)
free(config->server_host); goto error;
free(config);
return NULL;
}
if (strcmp(config->version, PROTOCOL_VERSION) != 0) { if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version, fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version,
PROTOCOL_VERSION); PROTOCOL_VERSION);
free(config->version);
free(config->server_host);
free(config);
send_status(file_descriptor, STATUS_ERROR); send_status(file_descriptor, STATUS_ERROR);
return NULL; goto error;
} }
config->send_directory = receive_str(file_descriptor); if (!receive_core_fields(file_descriptor, config) ||
if (!config->send_directory) { !receive_delta_fields(file_descriptor, config) ||
free(config->version); !receive_file_options(file_descriptor, config) ||
free(config->server_host); !receive_selection_options(file_descriptor, config) ||
free(config); !receive_resume_options(file_descriptor, config))
return NULL; 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->receive_root_directory = receive_str(file_descriptor);
if (!config->receive_root_directory) {
free(config->version);
free(config->send_directory);
free(config->server_host);
free(config);
return NULL;
}
int tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->save_to_disk = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_multithreading = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_chunk_serialization = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_compression = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_metadata = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->compression_level = tmp;
if (!receive_n_data(file_descriptor, &config->chunk_size, sizeof(config->chunk_size)))
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_sendfile = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_delete = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_incremental = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->use_delta = tmp;
if (!receive_n_data(file_descriptor, &config->delta_block_size, sizeof(config->delta_block_size)))
goto error;
if (!receive_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long)))
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->backup = tmp;
config->backup_dir = receive_str(file_descriptor);
if (config->backup_dir == NULL)
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->follow_symlinks = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->copy_links = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->safe_links = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->copy_unsafe_links = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->preserve_hard_links = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->preserve_acls = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->preserve_xattrs = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->preserve_devices = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->preserve_sparse = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->update = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->inplace = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->append = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->append_verify = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->delete_excluded = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->delete_after = tmp;
if (!receive_n_data(file_descriptor, &config->max_delete, sizeof(config->max_delete)))
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->relative = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->prune_empty_dirs = tmp;
config->temp_dir = receive_str(file_descriptor);
if (config->temp_dir == NULL)
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->partial = tmp;
config->partial_dir = receive_str(file_descriptor);
if (config->partial_dir == NULL)
goto error;
config->suffix = receive_str(file_descriptor);
if (config->suffix == NULL)
goto error;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->delete_before = tmp;
if (!receive_int(file_descriptor, &tmp))
goto error;
config->checksum = tmp;
config->compress_choice = receive_str(file_descriptor);
if (config->compress_choice == NULL)
goto error;
if (!send_status(file_descriptor, STATUS_OK)) if (!send_status(file_descriptor, STATUS_OK))
goto error; goto error;
return config; return config;
error: error:
free(config->version); config_delete(config);
free(config->send_directory);
free(config->receive_root_directory);
free(config->server_host);
free(config->backup_dir);
free(config->temp_dir);
free(config->partial_dir);
free(config->suffix);
free(config->compress_choice);
free(config);
return NULL; return NULL;
} }
+1 -2
View File
@@ -128,8 +128,7 @@ typedef struct Config {
char* compress_choice; char* compress_choice;
} Config; } Config;
/* This version must be bumped whenever config_send / config_receive wire format changes. */ #define PROTOCOL_VERSION "2.2.0"
#define PROTOCOL_VERSION "2.1.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
Config* config_create(void); Config* config_create(void);
+4
View File
@@ -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)
+1
View File
@@ -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
+245 -145
View File
@@ -2,6 +2,8 @@
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <libgen.h> #include <libgen.h>
#include <limits.h>
#include <poll.h>
#include <stddef.h> #include <stddef.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -9,6 +11,7 @@
#include <sys/sendfile.h> #include <sys/sendfile.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <time.h>
#include "compression.h" #include "compression.h"
#include "delta.h" #include "delta.h"
@@ -16,11 +19,27 @@
#include "config.h" #include "config.h"
#include "data.h" #include "data.h"
#include "file.h" #include "file.h"
#include "file_store.h"
#include "metadata.h" #include "metadata.h"
#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) {
if (!path)
return NULL;
File* file = (File*)malloc(sizeof(File)); File* file = (File*)malloc(sizeof(File));
if (file == NULL) { if (file == NULL) {
perror("ERROR: Could not allocate memory for file struct"); perror("ERROR: Could not allocate memory for file struct");
@@ -86,6 +105,8 @@ bool file_load_data(File* file) {
if (file == NULL) if (file == NULL)
return false; return false;
if (file->data->data == NULL) { if (file->data->data == NULL) {
if (file->data->size == 0)
return true;
file->data->data = malloc(file->data->size); file->data->data = malloc(file->data->size);
if (file->data->data == NULL) { if (file->data->data == NULL) {
perror("Could not allocate memory for file data"); perror("Could not allocate memory for file data");
@@ -105,6 +126,8 @@ bool file_load_data(File* file) {
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) {
if (!file || !file->path || !file->data)
return false;
const Data* data_to_send = file->data; const Data* data_to_send = file->data;
Data* compressed_data = NULL; Data* compressed_data = NULL;
if (compression_level > 0 && !compression_should_skip(file->path)) { if (compression_level > 0 && !compression_should_skip(file->path)) {
@@ -131,22 +154,47 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
return true; return true;
} }
bool file_save_to_disk(const char* root_directory, File* file, const Config* config) { void file_set_authorized_root(int fd, const char* canonical_path) {
file_store_set_authorized_root(fd, canonical_path);
}
static bool path_is_within_root(const char* root, const char* path) {
size_t n = strlen(root);
return strncmp(root, path, n) == 0 && (path[n] == '\0' || path[n] == '/');
}
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
bool backup_enabled = config && config->backup; bool backup_enabled = config && config->backup;
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 : "~";
const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL; const char* backup_dir = (config && config->backup_dir) ? config->backup_dir : NULL;
const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL; const char* partial_dir = (config && config->partial_dir) ? config->partial_dir : NULL;
char *confined_backup = NULL, *confined_partial = NULL;
if (has_path_traversal(file->path)) { if (!file || !file->path || !file->data || has_path_traversal(file->path) ||
log_message(LOG_LEVEL_ERROR, "Path traversal detected in file path: %s", file->path); (backup_enabled &&
(!backup_suffix || backup_suffix[0] == '\0' || strchr(backup_suffix, '/') != NULL ||
strcmp(backup_suffix, ".") == 0 || strcmp(backup_suffix, "..") == 0))) {
log_message(LOG_LEVEL_ERROR, "Invalid file or path received");
return false;
}
/* These options arrive from the client. They are names below the server
root, never independent filesystem roots. */
if ((backup_dir && (backup_dir[0] == '/' || has_path_traversal(backup_dir))) ||
(partial_dir && (partial_dir[0] == '/' || has_path_traversal(partial_dir))))
return false;
if (backup_dir && !(confined_backup = path_cat(root_directory, backup_dir)))
return false;
if (partial_dir && !(confined_partial = path_cat(root_directory, partial_dir))) {
free(confined_backup);
return false; return false;
} }
char* resolved_root = NULL; char* resolved_root = NULL;
const char* actual_root = const char* actual_root =
(partial_dir && config && config->partial) ? partial_dir : root_directory; (partial_dir && config && config->partial) ? confined_partial : root_directory;
resolved_root = realpath(actual_root, NULL); resolved_root = realpath(actual_root, NULL);
if (resolved_root == NULL) { if (resolved_root == NULL) {
if (mkdir_r(actual_root)) { if (mkdir_r(actual_root)) {
@@ -155,27 +203,56 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
} }
if (resolved_root == NULL) { if (resolved_root == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root); log_message(LOG_LEVEL_ERROR, "Failed to resolve destination root: %s", actual_root);
free(confined_backup);
free(confined_partial);
return false; return false;
} }
char* resolved_base = realpath(root_directory, NULL);
if (resolved_base == NULL || !path_is_within_root(resolved_base, resolved_root)) {
free(resolved_base);
free(confined_backup);
free(confined_partial);
free(resolved_root);
return false;
}
free(resolved_base);
char* disk_path = path_cat(resolved_root, file->path); char* disk_path = path_cat(resolved_root, file->path);
if (disk_path == NULL) { if (disk_path == NULL) {
free(confined_backup);
free(confined_partial);
free(resolved_root); free(resolved_root);
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(confined_backup);
free(confined_partial);
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) {
char* backup_path = NULL; char* backup_path = NULL;
if (backup_dir) { if (backup_dir) {
char* resolved_backup_dir = realpath(backup_dir, NULL); char* resolved_backup_dir = realpath(confined_backup, NULL);
if (!resolved_backup_dir) { if (!resolved_backup_dir) {
mkdir_r(backup_dir); mkdir_r(confined_backup);
resolved_backup_dir = realpath(backup_dir, NULL); resolved_backup_dir = realpath(confined_backup, NULL);
} }
if (resolved_backup_dir) { if (resolved_backup_dir) {
backup_path = path_cat(resolved_backup_dir, file->path); char* backup_base = realpath(root_directory, NULL);
if (backup_base && path_is_within_root(backup_base, resolved_backup_dir))
backup_path = path_cat(resolved_backup_dir, file->path);
free(backup_base);
free(resolved_backup_dir); free(resolved_backup_dir);
} }
} }
@@ -195,7 +272,14 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
mkdir_r(bdir); mkdir_r(bdir);
free(backup_dir_path); free(backup_dir_path);
} }
rename(disk_path, backup_path); if (!file_store_rename_secure(disk_path, backup_path)) {
free(backup_path);
free(resolved_root);
free(confined_backup);
free(confined_partial);
free(disk_path);
return false;
}
free(backup_path); free(backup_path);
} }
} }
@@ -203,6 +287,8 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
char* dir_dup = str_dup(disk_path); char* dir_dup = str_dup(disk_path);
if (!dir_dup) { if (!dir_dup) {
free(confined_backup);
free(confined_partial);
free(resolved_root); free(resolved_root);
free(disk_path); free(disk_path);
return false; return false;
@@ -210,6 +296,8 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
char* dir_str = dirname(dir_dup); char* dir_str = dirname(dir_dup);
if (!mkdir_r(dir_str)) { if (!mkdir_r(dir_str)) {
free(dir_dup); free(dir_dup);
free(confined_backup);
free(confined_partial);
free(resolved_root); free(resolved_root);
free(disk_path); free(disk_path);
return false; return false;
@@ -218,6 +306,8 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
free(dir_dup); free(dir_dup);
if (resolved_dir == NULL) { if (resolved_dir == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to resolve directory for: %s", disk_path); log_message(LOG_LEVEL_ERROR, "Failed to resolve directory for: %s", disk_path);
free(confined_backup);
free(confined_partial);
free(resolved_root); free(resolved_root);
free(disk_path); free(disk_path);
return false; return false;
@@ -228,6 +318,8 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
(resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) { (resolved_dir[root_len] != '\0' && resolved_dir[root_len] != '/')) {
log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root); log_message(LOG_LEVEL_ERROR, "Path escape detected: %s is outside %s", disk_path, actual_root);
free(resolved_dir); free(resolved_dir);
free(confined_backup);
free(confined_partial);
free(resolved_root); free(resolved_root);
free(disk_path); free(disk_path);
return false; return false;
@@ -235,31 +327,14 @@ bool file_save_to_disk(const char* root_directory, File* file, const Config* con
free(resolved_dir); free(resolved_dir);
free(resolved_root); free(resolved_root);
bool ok = to_disk(disk_path, file->data->data, file->data->size, inplace, sparse); bool ok = file_store_write_secure(disk_path, file->data->data, file->data->size, inplace, sparse,
if (ok) file->metadata);
file_restore_metadata(disk_path, file->metadata); free(confined_backup);
free(confined_partial);
free(disk_path); free(disk_path);
return ok; return ok;
} }
static void* old_data_from_path(const char* full_path, unsigned long long old_size) {
void* data = malloc((size_t)old_size);
if (!data)
return NULL;
FILE* fp = fopen(full_path, "rb");
if (!fp) {
free(data);
return NULL;
}
size_t nread = fread(data, 1, (size_t)old_size, fp);
fclose(fp);
if (nread != (size_t)old_size) {
free(data);
return NULL;
}
return data;
}
static File* receive_delta_file(int fd, const Config* config, const char* check_path, static File* receive_delta_file(int fd, const Config* config, const char* check_path,
void* old_data, unsigned long long old_size) { void* old_data, unsigned long long old_size) {
if (!old_data) if (!old_data)
@@ -423,12 +498,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);
@@ -439,13 +520,53 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
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_file = (full_path && lstat(full_path, &st) == 0); bool has_old_file = false;
int old_fd = -1;
if (full_path) {
char* leaf = NULL;
int parent_fd = file_store_open_secure_parent(full_path, &leaf);
if (parent_fd >= 0) {
old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
free(leaf);
close(parent_fd);
has_old_file = old_fd >= 0 && fstat(old_fd, &st) == 0 && S_ISREG(st.st_mode);
}
}
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;
void* old_data = NULL;
if (has_old_file && old_size > 0) {
old_data = malloc((size_t)old_size);
if (old_data) {
size_t got = 0;
while (got < (size_t)old_size) {
ssize_t n = read(old_fd, (char*)old_data + got, (size_t)old_size - got);
if (n <= 0) {
free(old_data);
old_data = NULL;
break;
}
got += (size_t)n;
}
}
}
if (old_fd >= 0) {
close(old_fd);
}
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) {
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);
old_data = NULL;
} else if (match) {
match = (long long)st.st_mtime == check_mtime;
}
if (match) { if (match) {
free(old_data);
if (!send_status(fd, STATUS_OK)) { if (!send_status(fd, STATUS_OK)) {
free(full_path); free(full_path);
free(check_path); free(check_path);
@@ -461,13 +582,15 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
delta_should_attempt(old_size, check_size, config->delta_max_file_size); delta_should_attempt(old_size, check_size, config->delta_max_file_size);
if (try_delta) { if (try_delta) {
void* old_data = old_data_from_path(full_path, old_size);
File* delta_file = receive_delta_file(fd, config, check_path, old_data, old_size); File* delta_file = receive_delta_file(fd, config, check_path, old_data, old_size);
old_data = NULL; /* receive_delta_file consumes the snapshot on every path */
if (delta_file) { if (delta_file) {
free(full_path); free(full_path);
free(check_path); free(check_path);
return delta_file; return delta_file;
} }
free(old_data);
old_data = NULL;
try_delta = false; try_delta = false;
} }
@@ -522,109 +645,15 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
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) {
char* tmp_path = NULL; if (!path || (!data && data_size != 0) || has_path_traversal(path))
char* directory = NULL;
char* path_dup = str_dup(path);
if (!path_dup)
return false; return false;
const char* dir_result = dirname(path_dup); return file_store_write_secure(path, data, data_size, inplace, sparse, NULL);
directory = str_dup(dir_result);
free(path_dup);
if (!directory)
return false;
bool ok = true;
if (!mkdir_r(directory))
goto done;
if (inplace) {
FILE* file_pointer = fopen(path, "wb");
if (file_pointer == NULL) {
perror("Could not open file for inplace write");
ok = false;
goto done;
}
if (sparse && data_size > 0) {
if (fseek(file_pointer, data_size - 1, SEEK_SET) != 0) {
perror("Failed to seek for sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
if (fwrite("", 1, 1, file_pointer) != 1) {
perror("Failed to write sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
rewind(file_pointer);
}
if (data_size > 0 && fwrite(data, 1, data_size, file_pointer) != data_size) {
perror("Failed to write all data to file");
fclose(file_pointer);
ok = false;
goto done;
}
fclose(file_pointer);
free(directory);
return true;
}
size_t path_len = strlen(path);
tmp_path = malloc(path_len + 5);
if (!tmp_path) {
ok = false;
goto done;
}
memcpy(tmp_path, path, path_len);
memcpy(tmp_path + path_len, ".tmp", 5);
FILE* file_pointer = fopen(tmp_path, "wb");
if (file_pointer == NULL) {
perror("Could not open temporary file");
ok = false;
goto done;
}
if (sparse && data_size > 0) {
if (fseek(file_pointer, data_size - 1, SEEK_SET) != 0) {
perror("Failed to seek for sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
if (fwrite("", 1, 1, file_pointer) != 1) {
perror("Failed to write sparse file");
fclose(file_pointer);
ok = false;
goto done;
}
rewind(file_pointer);
}
if (fwrite(data, 1, data_size, file_pointer) != data_size) {
perror("Failed to write all data to temporary file");
fclose(file_pointer);
unlink(tmp_path);
ok = false;
goto done;
}
fclose(file_pointer);
if (rename(tmp_path, path) != 0) {
perror("Failed to atomically rename temporary file");
unlink(tmp_path);
ok = false;
goto done;
}
done:
free(tmp_path);
free(directory);
return ok;
} }
bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level, bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level,
bool send_path) { bool send_path) {
if (!file || !file->path || !file->data)
return false;
if (compression_level > 0) if (compression_level > 0)
return file_send_single_calls(file, file_descriptor, use_metadata, compression_level, return file_send_single_calls(file, file_descriptor, use_metadata, compression_level,
send_path); send_path);
@@ -641,13 +670,56 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
} }
unsigned long long file_size = file->data->size; unsigned long long file_size = file->data->size;
struct stat source_stat;
if (fstat(fd, &source_stat) != 0 || !S_ISREG(source_stat.st_mode) ||
(unsigned long long)source_stat.st_size < file_size) {
close(fd);
return false;
}
if (!send_n_data(file_descriptor, &file_size, sizeof(unsigned long long))) { if (!send_n_data(file_descriptor, &file_size, sizeof(unsigned long long))) {
close(fd); close(fd);
return false; return false;
} }
/* sendfile cannot encrypt TLS records. Keep the framing identical but
route encrypted transfers through the deadline-aware IO layer. */
if (io_get_ssl() != NULL) {
unsigned char buffer[64 * 1024];
unsigned long long remaining = file_size;
bool ok = true;
while (remaining > 0) {
size_t want = remaining > sizeof(buffer) ? sizeof(buffer) : (size_t)remaining;
ssize_t got = read(fd, buffer, want);
if (got <= 0 || !send_n_data(file_descriptor, buffer, (size_t)got)) {
ok = false;
break;
}
remaining -= (unsigned long long)got;
}
close(fd);
return ok;
}
off_t offset = 0; off_t offset = 0;
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += 60;
while ((unsigned long long)offset < file_size) { while ((unsigned long long)offset < file_size) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long long remaining = (long long)(deadline.tv_sec - now.tv_sec) * 1000LL +
(deadline.tv_nsec - now.tv_nsec) / 1000000LL;
if (remaining <= 0) {
close(fd);
return false;
}
struct pollfd pfd = {.fd = file_descriptor, .events = POLLOUT};
int timeout = remaining > INT_MAX ? INT_MAX : (int)remaining;
int polled = poll(&pfd, 1, timeout);
if (polled <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) {
close(fd);
return false;
}
ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset); ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset);
if (sent == -1) { if (sent == -1) {
if (errno == EAGAIN || errno == EINTR) if (errno == EAGAIN || errno == EINTR)
@@ -656,6 +728,10 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
close(fd); close(fd);
return false; return false;
} }
if (sent == 0) {
close(fd);
return false;
}
} }
close(fd); close(fd);
@@ -666,6 +742,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)
@@ -714,21 +795,40 @@ size_t file_content_to_buffer(File* file) {
} }
int receive_manifest(int fd, const Config* config, int* next_status) { int receive_manifest(int fd, const Config* config, int* next_status) {
int received_status = STATUS_ERROR;
int* status_out = next_status ? next_status : &received_status;
int count; int count;
if (!receive_int(fd, &count)) if (!receive_int(fd, &count))
return -1; return -1;
ArrayList* manifest = array_list_create(free); if (count < 0 || count > MAX_MANIFEST_ENTRIES)
if (manifest) {
for (int i = 0; i < count; i++) {
char* s = receive_str(fd);
if (s)
array_list_add(manifest, s);
}
fprintf(stderr, "Deleting files not in manifest...\n");
delete_extras(config->receive_root_directory, manifest);
array_list_delete(manifest);
}
if (!receive_status(fd, next_status))
return -1; return -1;
return 0; ArrayList* manifest = array_list_create(free);
if (!manifest)
return -1;
size_t manifest_bytes = 0;
for (int i = 0; i < count; i++) {
char* s = receive_str(fd);
size_t entry_size = s ? strlen(s) : 0;
if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) ||
entry_size > MAX_MANIFEST_BYTES - manifest_bytes ||
(manifest_bytes += entry_size) > MAX_MANIFEST_BYTES || !array_list_add(manifest, s)) {
free(s);
array_list_delete(manifest);
return -1;
}
}
if (!receive_status(fd, status_out)) {
array_list_delete(manifest);
return -1;
}
/* Deletion is a commit operation: never perform it until the sender has
completed the manifest frame successfully. */
if (*status_out != STATUS_FINISHED || !config->use_delete) {
array_list_delete(manifest);
return *status_out == STATUS_FINISHED ? 0 : -1;
}
fprintf(stderr, "Deleting files not in manifest...\n");
bool deletion_ok = delete_extras(config->receive_root_directory, manifest);
array_list_delete(manifest);
return deletion_ok ? 0 : -1;
} }
+3 -1
View File
@@ -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);
@@ -36,7 +37,8 @@ FileMetadata* file_metadata_create(const struct stat* stats);
void file_metadata_destroy(void* metadata); void file_metadata_destroy(void* metadata);
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);
bool file_save_to_disk(const char* root_directory, File* file, const Config* config); bool file_save_to_disk(const char* root_directory, const File* file, const Config* config);
void file_set_authorized_root(int fd, const char* canonical_path);
File* receive_incremental_check(int fd, const Config* config, bool* skipped); File* receive_incremental_check(int fd, const Config* config, bool* skipped);
int receive_manifest(int fd, const Config* config, int* next_status); int receive_manifest(int fd, const Config* config, int* next_status);
+159
View File
@@ -0,0 +1,159 @@
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "file_store.h"
#include "metadata.h"
#include "utils.h"
static int authorized_root_fd = -1;
static char* authorized_root_path;
static bool path_is_within_root(const char* root, const char* path) {
size_t root_length = strlen(root);
return strncmp(root, path, root_length) == 0 &&
(path[root_length] == '\0' || path[root_length] == '/');
}
void file_store_set_authorized_root(int fd, const char* canonical_path) {
authorized_root_fd = fd;
free(authorized_root_path);
authorized_root_path = canonical_path ? str_dup(canonical_path) : NULL;
}
int file_store_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;
if (authorized_root_fd >= 0 && authorized_root_path && path[0] == '/' &&
path_is_within_root(authorized_root_path, path)) {
fd = dup(authorized_root_fd);
size_t root_length = strlen(authorized_root_path);
char* relative = str_dup(path + root_length);
if (!relative) {
free(copy);
free(leaf);
close(fd);
return -1;
}
free(copy);
copy = relative;
parent = dirname(copy);
} else {
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;
}
bool file_store_rename_secure(const char* old_path, const char* new_path) {
char *old_leaf = NULL, *new_leaf = NULL;
int old_parent = file_store_open_secure_parent(old_path, &old_leaf);
int new_parent = file_store_open_secure_parent(new_path, &new_leaf);
bool ok = old_parent >= 0 && new_parent >= 0 &&
renameat(old_parent, old_leaf, new_parent, new_leaf) == 0;
if (old_parent >= 0)
close(old_parent);
if (new_parent >= 0)
close(new_parent);
free(old_leaf);
free(new_leaf);
return ok;
}
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;
}
bool file_store_write_secure(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse, const FileMetadata* metadata) {
char* leaf = NULL;
int dirfd = file_store_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);
if (ok && metadata)
ok = file_restore_metadata_fd(fd, metadata);
}
} 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 (ok && metadata)
ok = file_restore_metadata_fd(fd, metadata);
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;
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef FILE_STORE_H
#define FILE_STORE_H
#include "file.h"
#include <stdbool.h>
void file_store_set_authorized_root(int fd, const char* canonical_path);
int file_store_open_secure_parent(const char* path, char** leaf_out);
bool file_store_rename_secure(const char* old_path, const char* new_path);
bool file_store_write_secure(const char* path, const void* data, unsigned long long data_size,
bool inplace, bool sparse, const FileMetadata* metadata);
#endif
+15 -9
View File
@@ -15,6 +15,15 @@ void log_set_file(FILE* fp) {
log_fp = fp; log_fp = fp;
} }
static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format,
va_list args) {
fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]);
vfprintf(dest_io, format, args);
fprintf(dest_io, "\n");
}
void log_message(LogLevel log_level, const char* format, ...) { void log_message(LogLevel log_level, const char* format, ...) {
if (log_level < current_log_level) if (log_level < current_log_level)
return; return;
@@ -25,22 +34,19 @@ void log_message(LogLevel log_level, const char* format, ...) {
if (!localtime_r(&now, &t)) if (!localtime_r(&now, &t))
return; return;
fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, FILE* dest_io = stdout;
t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); if (log_level == LOG_LEVEL_ERROR) {
dest_io = stderr;
}
va_list args; va_list args;
va_start(args, format); va_start(args, format);
vfprintf(stderr, format, args); write_message(dest_io, log_level, t, format, args);
va_end(args); va_end(args);
fprintf(stderr, "\n");
if (log_fp) { if (log_fp) {
fprintf(log_fp, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1,
t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]);
va_start(args, format); va_start(args, format);
vfprintf(log_fp, format, args); write_message(log_fp, log_level, t, format, args);
va_end(args); va_end(args);
fprintf(log_fp, "\n");
fflush(log_fp);
} }
} }
+29 -4
View File
@@ -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,18 +161,24 @@ 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;
} }
void file_restore_metadata(const char* path, FileMetadata* metadata) { void file_restore_metadata(const char* path, const FileMetadata* metadata) {
if (metadata == NULL) if (metadata == NULL)
return; return;
if (chmod(path, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 0) if (chmod(path, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 0)
log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno)); log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno));
if (chown(path, metadata->uid, metadata->gid) != 0) /* Never apply client-supplied ownership. The descriptor API below is the
log_message(LOG_LEVEL_WARNING, "Failed to chown %s: %s", path, strerror(errno)); receiver write path; retain this legacy API only for compatibility. */
struct timespec times[2]; struct timespec times[2];
times[0].tv_sec = 0; times[0].tv_sec = 0;
times[0].tv_nsec = UTIME_OMIT; times[0].tv_nsec = UTIME_OMIT;
@@ -176,3 +187,17 @@ void file_restore_metadata(const char* path, FileMetadata* metadata) {
if (utimensat(AT_FDCWD, path, times, 0) != 0) if (utimensat(AT_FDCWD, path, times, 0) != 0)
log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", path, strerror(errno)); log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", path, strerror(errno));
} }
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata) {
if (fd < 0 || metadata == NULL)
return metadata == NULL;
bool ok = true;
if (fchmod(fd, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 0)
ok = false;
/* Client uid/gid values are deliberately not authoritative. */
struct timespec times[2] = {{.tv_sec = 0, .tv_nsec = UTIME_OMIT},
{.tv_sec = metadata->mtime_sec, .tv_nsec = metadata->mtime_nsec}};
if (futimens(fd, times) != 0)
ok = false;
return ok;
}
+2 -1
View File
@@ -29,6 +29,7 @@ void metadata_to_buf(char** buf, const FileMetadata* m);
FileMetadata* metadata_from_buf(char** buf); FileMetadata* metadata_from_buf(char** buf);
bool metadata_send(int file_descriptor, FileMetadata* m); bool metadata_send(int file_descriptor, FileMetadata* m);
FileMetadata* metadata_receive(int file_descriptor, int* ok); FileMetadata* metadata_receive(int file_descriptor, int* ok);
void file_restore_metadata(const char* path, FileMetadata* metadata); void file_restore_metadata(const char* path, const FileMetadata* metadata);
bool file_restore_metadata_fd(int fd, const FileMetadata* metadata);
#endif #endif
+39 -102
View File
@@ -1,4 +1,6 @@
#include "multiprocessing.h" #include "multiprocessing.h"
#include "receiver.h"
#include "array_list.h" #include "array_list.h"
#include "chunk.h" #include "chunk.h"
#include "config.h" #include "config.h"
@@ -26,7 +28,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
context->manifest = NULL; context->manifest = NULL;
context->progress_bytes = 0; context->progress_bytes = 0;
context->sender_done = false; context->sender_done = false;
context->cancelled = false; atomic_init(&context->cancelled, false);
int init = 0; int init = 0;
if (mtx_init(&context->mutex_scanner, mtx_plain) != thrd_success) if (mtx_init(&context->mutex_scanner, mtx_plain) != thrd_success)
goto fail; goto fail;
@@ -96,8 +98,10 @@ PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue*
context->queue = queue; context->queue = queue;
context->file_descriptor = file_descriptor; context->file_descriptor = file_descriptor;
context->ssl = ssl; context->ssl = ssl;
protocol_session_init(&context->session, file_descriptor, file_descriptor);
protocol_session_set_ssl(&context->session, ssl);
context->receiver_done = false; context->receiver_done = false;
context->cancelled = false; atomic_init(&context->cancelled, false);
int init = 0; int init = 0;
if (mtx_init(&context->mutex, mtx_plain) != thrd_success) if (mtx_init(&context->mutex, mtx_plain) != thrd_success)
goto fail; goto fail;
@@ -132,125 +136,49 @@ void pipeline_context_receiver_destroy(PipelineContextReceiver* context) {
free(context); free(context);
} }
static bool receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver* context) { static bool receiver_enqueue_file(File* file, void* context_pointer) {
Chunk* chunk = receive_chunk_data(file_descriptor, context->config); PipelineContextReceiver* context = context_pointer;
if (chunk == NULL) if (queue_enqueue_multithreaded_cancel(context->queue, file, &context->mutex,
return false; &context->condition_not_empty,
&context->condition_not_full, &context->cancelled))
return true;
file_destroy(file);
return false;
}
for (int i = 0; i < chunk->element_count; i++) { static void receiver_thread_fail(PipelineContextReceiver* context) {
File* file = chunk->items[i]; mtx_lock(&context->mutex);
chunk->items[i] = NULL; atomic_store(&context->cancelled, true);
if (!queue_enqueue_multithreaded_cancel(context->queue, file, &context->mutex, context->receiver_done = true;
&context->condition_not_empty, cnd_broadcast(&context->condition_not_empty);
&context->condition_not_full, &context->cancelled)) { cnd_broadcast(&context->condition_not_full);
file_destroy(file); mtx_unlock(&context->mutex);
chunk_destroy(chunk);
return false;
}
}
chunk_destroy(chunk);
return true;
} }
int receive_thread(void* pipeline_context) { int receive_thread(void* pipeline_context) {
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context; PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
if (context->ssl) protocol_session_bind(&context->session);
io_set_ssl(context->ssl);
mtx_lock(&context->mutex); mtx_lock(&context->mutex);
int file_descriptor = context->file_descriptor; int file_descriptor = context->file_descriptor;
const Config* config = context->config; const Config* config = context->config;
mtx_unlock(&context->mutex); mtx_unlock(&context->mutex);
Status status; ReceiverSink sink = {receiver_enqueue_file, context, false, false};
if (!receive_status(file_descriptor, &status)) if (receiver_process((Config*)config, file_descriptor, &sink) != 0) {
receiver_thread_fail(context);
protocol_session_unbind();
return thrd_error; return thrd_error;
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
if (status == STATUS_KEEPALIVE) {
send_status(file_descriptor, STATUS_KEEPALIVE);
goto next;
}
if (status == STATUS_ABORT) {
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
return thrd_error;
}
if (status == STATUS_CHECK) {
bool skipped;
File* file = receive_incremental_check(file_descriptor, config, &skipped);
if (!skipped) {
if (file == NULL)
return thrd_error;
if (!queue_enqueue_multithreaded_cancel(
context->queue, file, &context->mutex, &context->condition_not_empty,
&context->condition_not_full, &context->cancelled)) {
file_destroy(file);
return thrd_error;
}
}
} else if (status == STATUS_CHUNK) {
if (!receive_chunk_enqueue(file_descriptor, context))
return thrd_error;
} else if (status == STATUS_CHECK_BATCH) {
int count;
if (!receive_int(file_descriptor, &count))
return thrd_error;
for (int i = 0; i < count; i++) {
char* check_path = receive_str(file_descriptor);
if (!check_path)
return thrd_error;
unsigned long long check_size;
long long check_mtime;
if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) ||
!receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime))) {
free(check_path);
return thrd_error;
}
char* full_path = path_cat(config->receive_root_directory, check_path);
struct stat st;
bool has_old = full_path && lstat(full_path, &st) == 0;
bool match = has_old && (unsigned long long)st.st_size == check_size &&
(long long)st.st_mtime == check_mtime;
if (match)
send_status(file_descriptor, STATUS_OK);
else
send_status(file_descriptor, STATUS_NEXT);
free(full_path);
free(check_path);
}
goto next;
} else {
File* file = file_receive(config, file_descriptor);
if (file) {
if (!queue_enqueue_multithreaded_cancel(
context->queue, file, &context->mutex, &context->condition_not_empty,
&context->condition_not_full, &context->cancelled)) {
file_destroy(file);
return thrd_error;
}
} else {
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
return thrd_error;
}
}
next:
if (!receive_status(file_descriptor, &status))
return thrd_error;
}
if (status == STATUS_MANIFEST) {
if (receive_manifest(file_descriptor, config, &status) != 0)
return thrd_error;
} }
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);
protocol_session_unbind();
return thrd_success; return thrd_success;
} }
int write_thread(void* pipeline_context) { int write_thread(void* pipeline_context) {
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context; PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
if (context->ssl)
io_set_ssl(context->ssl);
mtx_lock(&context->mutex); mtx_lock(&context->mutex);
bool save_to_disk = context->config->save_to_disk; bool save_to_disk = context->config->save_to_disk;
char* root_directory = str_dup(context->config->receive_root_directory); char* root_directory = str_dup(context->config->receive_root_directory);
@@ -264,8 +192,17 @@ int write_thread(void* pipeline_context) {
free(root_directory); free(root_directory);
return thrd_success; return thrd_success;
} }
if (save_to_disk) if (save_to_disk && !file_save_to_disk(root_directory, file, context->config)) {
file_save_to_disk(root_directory, file, context->config); file_destroy(file);
mtx_lock(&context->mutex);
atomic_store(&context->cancelled, true);
context->receiver_done = true;
cnd_broadcast(&context->condition_not_full);
cnd_broadcast(&context->condition_not_empty);
mtx_unlock(&context->mutex);
free(root_directory);
return thrd_error;
}
file_destroy(file); file_destroy(file);
} }
} }
+4 -2
View File
@@ -2,6 +2,7 @@
#define MULTIPROCESSING_H #define MULTIPROCESSING_H
#include <threads.h> #include <threads.h>
#include <stdatomic.h>
#include "array_list.h" #include "array_list.h"
#include "config.h" #include "config.h"
@@ -26,7 +27,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; atomic_bool cancelled;
} PipelineContextSender; } PipelineContextSender;
typedef struct PipelineContextReceiver { typedef struct PipelineContextReceiver {
@@ -34,11 +35,12 @@ typedef struct PipelineContextReceiver {
Config* config; Config* config;
int file_descriptor; int file_descriptor;
SSL* ssl; SSL* ssl;
ProtocolSession session;
mtx_t mutex; mtx_t mutex;
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; atomic_bool cancelled;
} PipelineContextReceiver; } PipelineContextReceiver;
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner, PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
+215 -66
View File
@@ -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>
@@ -10,12 +11,14 @@
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */ #define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */
#define SEND_TIMEOUT_SEC 60
#define MAX_CONNECTION_MEMORY (1024ULL * 1024 * 1024) /* 1 GB total per connection */ #define MAX_CONNECTION_MEMORY (1024ULL * 1024 * 1024) /* 1 GB total per connection */
static __thread int io_read_fd = -1; static __thread int io_read_fd = -1;
static __thread int io_write_fd = -1; static __thread int io_write_fd = -1;
static __thread SSL* io_ssl; static __thread SSL* io_ssl;
static __thread ProtocolSession* bound_session;
static unsigned long long io_bwlimit = 0; static unsigned long long io_bwlimit = 0;
static long long bw_tokens = 0; static long long bw_tokens = 0;
@@ -26,8 +29,36 @@ static once_flag bw_mutex_once = ONCE_FLAG_INIT;
static __thread unsigned long long total_allocated_bytes = 0; static __thread unsigned long long total_allocated_bytes = 0;
void io_set_fds(int read_fd, int write_fd) { void io_set_fds(int read_fd, int write_fd) {
bound_session = NULL;
io_read_fd = read_fd; io_read_fd = read_fd;
io_write_fd = write_fd; io_write_fd = write_fd;
/* A descriptor switch starts a new transport; never reuse a TLS object
belonging to a previous connection or test pipe. */
io_ssl = NULL;
total_allocated_bytes = 0;
}
void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd) {
if (!session)
return;
memset(session, 0, sizeof(*session));
session->read_fd = read_fd;
session->write_fd = write_fd;
if (io_bwlimit)
protocol_session_set_bwlimit(session, io_bwlimit);
}
void protocol_session_bind(ProtocolSession* session) {
bound_session = session;
}
void protocol_session_unbind(void) {
bound_session = NULL;
}
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl) {
if (session)
session->ssl = ssl;
} }
static void bw_mutex_init(void) { static void bw_mutex_init(void) {
@@ -43,39 +74,51 @@ void io_set_bwlimit(unsigned long long bytes_per_sec) {
mtx_unlock(&bw_mutex); mtx_unlock(&bw_mutex);
} }
static void bw_throttle(size_t bytes_written) { void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec) {
if (io_bwlimit == 0) if (!session)
return;
session->bwlimit = bytes_per_sec;
session->bw_tokens = (long long)bytes_per_sec;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
session->bw_last_refill_sec = now.tv_sec;
session->bw_last_refill_nsec = now.tv_nsec;
}
static void bw_throttle_session(ProtocolSession* session, size_t bytes_written) {
if (session->bwlimit == 0)
return; return;
call_once(&bw_mutex_once, bw_mutex_init);
mtx_lock(&bw_mutex);
struct timespec now; struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now); clock_gettime(CLOCK_MONOTONIC, &now);
long long elapsed_ns = long long elapsed_ns = (now.tv_sec - session->bw_last_refill_sec) * 1000000000LL +
(now.tv_sec - bw_last_refill.tv_sec) * 1000000000LL + (now.tv_nsec - bw_last_refill.tv_nsec); (now.tv_nsec - session->bw_last_refill_nsec);
bw_last_refill = now; session->bw_last_refill_sec = now.tv_sec;
session->bw_last_refill_nsec = now.tv_nsec;
long long tokens_to_add = (long long)((double)io_bwlimit * elapsed_ns / 1000000000.0); long long tokens_to_add = (long long)((double)session->bwlimit * elapsed_ns / 1000000000.0);
bw_tokens += tokens_to_add; session->bw_tokens += tokens_to_add;
if (bw_tokens > (long long)io_bwlimit) if (session->bw_tokens > (long long)session->bwlimit)
bw_tokens = (long long)io_bwlimit; session->bw_tokens = (long long)session->bwlimit;
bw_tokens -= (long long)bytes_written; session->bw_tokens -= bytes_written;
if (bw_tokens < 0) { if (session->bw_tokens < 0) {
long long deficit_us = (long long)((double)(-bw_tokens) / io_bwlimit * 1000000.0); long long deficit_us =
(long long)((double)(-session->bw_tokens) / session->bwlimit * 1000000.0);
if (deficit_us >= 1000) if (deficit_us >= 1000)
poll(NULL, 0, (int)(deficit_us / 1000)); poll(NULL, 0, (int)(deficit_us / 1000));
else else
usleep((useconds_t)deficit_us); usleep((useconds_t)deficit_us);
bw_tokens = 0; session->bw_tokens = 0;
clock_gettime(CLOCK_MONOTONIC, &bw_last_refill); session->bw_last_refill_sec = now.tv_sec;
session->bw_last_refill_nsec = now.tv_nsec;
} }
mtx_unlock(&bw_mutex);
} }
void io_set_ssl(SSL* ssl) { void io_set_ssl(SSL* ssl) {
bound_session = NULL;
io_ssl = ssl; io_ssl = ssl;
} }
@@ -83,69 +126,134 @@ SSL* io_get_ssl(void) {
return io_ssl; return io_ssl;
} }
static int io_fd(int dir_fd, int file_descriptor) { static ProtocolSession* legacy_session(void) {
return (dir_fd != -1) ? dir_fd : file_descriptor; static __thread ProtocolSession session;
if (bound_session)
return bound_session;
session.read_fd = io_read_fd;
session.write_fd = io_write_fd;
session.ssl = io_ssl;
session.bwlimit = io_bwlimit;
session.bw_tokens = (unsigned long long)(bw_tokens < 0 ? 0 : bw_tokens);
session.bw_last_refill_sec = bw_last_refill.tv_sec;
session.bw_last_refill_nsec = bw_last_refill.tv_nsec;
return &session;
} }
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) {
ProtocolSession* session = legacy_session();
if (session->write_fd == -1)
session->write_fd = file_descriptor;
return protocol_send_n_data(session, data, data_size);
}
bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
ProtocolSession* session = legacy_session();
if (session->read_fd == -1)
session->read_fd = file_descriptor;
return protocol_receive_n_data(session, data, data_size);
}
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 protocol_send_n_data(ProtocolSession* session, 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); if (!session)
return false;
int fd = session->write_fd;
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += SEND_TIMEOUT_SEC;
short wait_events = POLLOUT;
ssize_t total_bytes_send = 0; ssize_t total_bytes_send = 0;
while ((size_t)total_bytes_send < data_size) { while ((size_t)total_bytes_send < data_size) {
size_t chunk = data_size - total_bytes_send; size_t chunk = data_size - total_bytes_send;
if (io_bwlimit > 0 && chunk > 65536) if (session->bwlimit > 0 && chunk > 65536)
chunk = 65536; chunk = 65536;
struct pollfd pfd = {.fd = fd, .events = wait_events};
int poll_result = poll(&pfd, 1, deadline_remaining_ms(&deadline));
if (poll_result == 0 || (poll_result < 0 && errno != EINTR)) {
log_message(LOG_LEVEL_ERROR, "Send timeout or poll failure");
return false;
}
if (poll_result < 0)
continue;
if (pfd.revents & (POLLERR | POLLNVAL))
return false;
ssize_t bytes_send; ssize_t bytes_send;
if (io_ssl) if (session->ssl)
bytes_send = SSL_write(io_ssl, (const char*)data + total_bytes_send, chunk); bytes_send = SSL_write(session->ssl, (const char*)data + total_bytes_send, chunk);
else else
bytes_send = write(fd, (const char*)data + total_bytes_send, chunk); bytes_send = write(fd, (const char*)data + total_bytes_send, chunk);
if (bytes_send <= 0) { if (bytes_send <= 0) {
if (io_ssl) { if (session->ssl) {
int ssl_err = SSL_get_error(io_ssl, (int)bytes_send); int ssl_err = SSL_get_error(session->ssl, (int)bytes_send);
if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ) if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ) {
wait_events = ssl_err == SSL_ERROR_WANT_WRITE ? POLLOUT : POLLIN;
continue; continue;
}
} }
log_message(LOG_LEVEL_ERROR, "Could not send data"); log_message(LOG_LEVEL_ERROR, "Could not send data");
return false; return false;
} }
bw_throttle((size_t)bytes_send); bw_throttle_session(session, (size_t)bytes_send);
total_bytes_send += bytes_send; total_bytes_send += bytes_send;
} }
log_message(LOG_LEVEL_DEBUG, " Send n Data: %zu", total_bytes_send); log_message(LOG_LEVEL_DEBUG, " Send n Data: %zu", total_bytes_send);
return true; return true;
} }
bool receive_n_data(int file_descriptor, void* data, size_t data_size) { bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size) {
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size); log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size);
int fd = io_fd(io_read_fd, file_descriptor); if (!session)
return false;
int fd = session->read_fd;
struct timespec deadline; struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline); clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += RECEIVE_TIMEOUT_SEC; deadline.tv_sec += RECEIVE_TIMEOUT_SEC;
size_t total_bytes_received = 0; size_t total_bytes_received = 0;
short wait_events = POLLIN;
while (total_bytes_received < data_size) { while (total_bytes_received < data_size) {
struct timespec now; struct pollfd pfd = {.fd = fd, .events = wait_events};
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;
}
/* POLLHUP may accompany the final readable bytes on pipes/sockets. */
if (pfd.revents & (POLLERR | POLLNVAL))
return false;
ssize_t bytes_received; ssize_t bytes_received;
if (io_ssl) if (session->ssl)
bytes_received = bytes_received = SSL_read(session->ssl, (char*)data + total_bytes_received,
SSL_read(io_ssl, (char*)data + total_bytes_received, data_size - total_bytes_received); data_size - total_bytes_received);
else else
bytes_received = bytes_received =
read(fd, (char*)data + total_bytes_received, data_size - total_bytes_received); read(fd, (char*)data + total_bytes_received, data_size - total_bytes_received);
if (bytes_received <= 0) { if (bytes_received <= 0) {
if (io_ssl) { if (session->ssl) {
int ssl_err = SSL_get_error(io_ssl, (int)bytes_received); int ssl_err = SSL_get_error(session->ssl, (int)bytes_received);
if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ) if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ) {
wait_events = ssl_err == SSL_ERROR_WANT_WRITE ? POLLOUT : POLLIN;
continue; continue;
}
} }
if (bytes_received == 0) if (bytes_received == 0)
log_message(LOG_LEVEL_ERROR, "Connection closed while receiving data"); log_message(LOG_LEVEL_ERROR, "Connection closed while receiving data");
@@ -188,23 +296,24 @@ static const char* status_to_string(Status status) {
} }
} }
bool send_str(int file_descriptor, const char* data) { bool protocol_send_str(ProtocolSession* session, const char* data) {
if (data == NULL) if (data == NULL)
return false; return false;
size_t size = strlen(data); size_t size = strlen(data);
if (!send_n_data(file_descriptor, &size, sizeof(size_t))) if (!protocol_send_n_data(session, &size, sizeof(size_t)))
return false; return false;
if (!send_n_data(file_descriptor, data, size)) if (!protocol_send_n_data(session, data, size))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Send String: %s", data); log_message(LOG_LEVEL_DEBUG, "Send String: %s", data);
return true; return true;
} }
char* receive_str(int file_descriptor) { char* protocol_receive_str(ProtocolSession* session) {
size_t size; size_t size;
if (!receive_n_data(file_descriptor, &size, sizeof(size_t))) if (!protocol_receive_n_data(session, &size, sizeof(size_t)))
return NULL; return NULL;
if (size > MAX_STRING_SIZE) { if (size > MAX_STRING_SIZE || size > SIZE_MAX - 1 ||
size + 1 > MAX_CONNECTION_MEMORY - session->total_allocated_bytes) {
log_message(LOG_LEVEL_ERROR, "String size %zu exceeds maximum %llu", size, log_message(LOG_LEVEL_ERROR, "String size %zu exceeds maximum %llu", size,
(unsigned long long)MAX_STRING_SIZE); (unsigned long long)MAX_STRING_SIZE);
return NULL; return NULL;
@@ -212,76 +321,116 @@ char* receive_str(int file_descriptor) {
char* data = (char*)malloc(size + 1); char* data = (char*)malloc(size + 1);
if (data == NULL) if (data == NULL)
return NULL; return NULL;
if (!receive_n_data(file_descriptor, data, size)) { if (!protocol_receive_n_data(session, data, size)) {
free(data); free(data);
return NULL; return NULL;
} }
data[size] = '\0'; data[size] = '\0';
session->total_allocated_bytes += size + 1;
log_message(LOG_LEVEL_DEBUG, "Received String: %s", data); log_message(LOG_LEVEL_DEBUG, "Received String: %s", data);
return data; return data;
} }
bool send_data(int file_descriptor, const Data* data) { bool protocol_send_data(ProtocolSession* session, const Data* data) {
unsigned long long data_size = data->size; unsigned long long data_size = data->size;
if (!send_n_data(file_descriptor, &data_size, sizeof(unsigned long long))) if (!protocol_send_n_data(session, &data_size, sizeof(unsigned long long)))
return false; return false;
if (!send_n_data(file_descriptor, data->data, data_size)) if (!protocol_send_n_data(session, data->data, data_size))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Send %lld data", data_size); log_message(LOG_LEVEL_DEBUG, "Send %lld data", data_size);
return true; return true;
} }
Data* receive_data(int file_descriptor) { Data* protocol_receive_data(ProtocolSession* session) {
unsigned long long size = 0; unsigned long long size = 0;
if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long))) if (!protocol_receive_n_data(session, &size, sizeof(unsigned long long)))
return NULL; return NULL;
if (size > MAX_DATA_PAYLOAD_SIZE) { if (size > MAX_DATA_PAYLOAD_SIZE) {
log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size, log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size,
(unsigned long long)MAX_DATA_PAYLOAD_SIZE); (unsigned long long)MAX_DATA_PAYLOAD_SIZE);
return NULL; return NULL;
} }
if (total_allocated_bytes + size > MAX_CONNECTION_MEMORY) { size_t allocation_size = size == 0 ? 1 : (size_t)size;
if (allocation_size > MAX_CONNECTION_MEMORY - session->total_allocated_bytes) {
log_message(LOG_LEVEL_ERROR, "Per-connection memory limit exceeded (%llu + %llu > %llu)", log_message(LOG_LEVEL_ERROR, "Per-connection memory limit exceeded (%llu + %llu > %llu)",
(unsigned long long)total_allocated_bytes, size, (unsigned long long)session->total_allocated_bytes, size,
(unsigned long long)MAX_CONNECTION_MEMORY); (unsigned long long)MAX_CONNECTION_MEMORY);
return NULL; return NULL;
} }
void* data = malloc((size_t)size); void* data = malloc(allocation_size);
if (data == NULL) if (data == NULL)
return NULL; return NULL;
if (!receive_n_data(file_descriptor, data, (size_t)size)) { if (!protocol_receive_n_data(session, data, (size_t)size)) {
free(data); free(data);
return NULL; return NULL;
} }
total_allocated_bytes += size; session->total_allocated_bytes += allocation_size;
log_message(LOG_LEVEL_DEBUG, "Received %lld data", size); log_message(LOG_LEVEL_DEBUG, "Received %lld data", size);
return data_create(data, (size_t)size); Data* result = data_create(data, (size_t)size);
if (!result) {
free(data);
session->total_allocated_bytes -= allocation_size;
}
return result;
} }
bool send_int(int file_descriptor, int data) { bool protocol_send_int(ProtocolSession* session, int data) {
if (!send_n_data(file_descriptor, &data, sizeof(int))) if (!protocol_send_n_data(session, &data, sizeof(int)))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Send Int: %d", data); log_message(LOG_LEVEL_DEBUG, "Send Int: %d", data);
return true; return true;
} }
bool receive_int(int file_descriptor, int* data) { bool protocol_receive_int(ProtocolSession* session, int* data) {
if (!receive_n_data(file_descriptor, data, sizeof(int))) if (!protocol_receive_n_data(session, data, sizeof(int)))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Received Int: %d", *data); log_message(LOG_LEVEL_DEBUG, "Received Int: %d", *data);
return true; return true;
} }
bool send_status(int file_descriptor, Status status) { bool protocol_send_status(ProtocolSession* session, Status status) {
if (!send_n_data(file_descriptor, &status, sizeof(Status))) if (!protocol_send_n_data(session, &status, sizeof(Status)))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Send Status: %s", status_to_string(status)); log_message(LOG_LEVEL_DEBUG, "Send Status: %s", status_to_string(status));
return true; return true;
} }
bool receive_status(int file_descriptor, Status* status) { bool protocol_receive_status(ProtocolSession* session, Status* status) {
if (!receive_n_data(file_descriptor, status, sizeof(Status))) if (!protocol_receive_n_data(session, status, sizeof(Status)))
return false; return false;
log_message(LOG_LEVEL_DEBUG, "Received Status: %s", status_to_string(*status)); log_message(LOG_LEVEL_DEBUG, "Received Status: %s", status_to_string(*status));
return true; return true;
} }
bool send_str(int fd, const char* data) {
(void)fd;
return protocol_send_str(legacy_session(), data);
}
char* receive_str(int fd) {
(void)fd;
return protocol_receive_str(legacy_session());
}
bool send_data(int fd, const Data* data) {
(void)fd;
return protocol_send_data(legacy_session(), data);
}
Data* receive_data(int fd) {
(void)fd;
return protocol_receive_data(legacy_session());
}
bool send_int(int fd, int data) {
(void)fd;
return protocol_send_int(legacy_session(), data);
}
bool receive_int(int fd, int* data) {
(void)fd;
return protocol_receive_int(legacy_session(), data);
}
bool send_status(int fd, Status status) {
(void)fd;
return protocol_send_status(legacy_session(), status);
}
bool receive_status(int fd, Status* status) {
(void)fd;
return protocol_receive_status(legacy_session(), status);
}
+37
View File
@@ -13,9 +13,29 @@
/* 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)
/* Aggregate bytes retained by one received deletion manifest. */
#define MAX_MANIFEST_BYTES (16ULL * 1024 * 1024)
typedef struct ssl_st SSL; typedef struct ssl_st SSL;
/*
* Explicit owner of protocol I/O. A session does not own the descriptors or
* SSL object; it only describes the transport used by a transfer. This makes
* it safe to pass the transport to a worker without relying on inherited
* thread-local state.
*/
typedef struct ProtocolSession {
int read_fd;
int write_fd;
SSL* ssl;
unsigned long long bwlimit;
long long bw_tokens;
long long bw_last_refill_sec;
long bw_last_refill_nsec;
unsigned long long total_allocated_bytes;
} ProtocolSession;
typedef int Status; typedef int Status;
enum NET_STATUS { enum NET_STATUS {
STATUS_OK, STATUS_OK,
@@ -36,6 +56,23 @@ void io_set_fds(int read_fd, int write_fd);
void io_set_bwlimit(unsigned long long bytes_per_sec); void io_set_bwlimit(unsigned long long bytes_per_sec);
void io_set_ssl(SSL* ssl); void io_set_ssl(SSL* ssl);
SSL* io_get_ssl(void); SSL* io_get_ssl(void);
void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd);
/* Transitional bridge for helpers whose signatures still carry only an fd. */
void protocol_session_bind(ProtocolSession* session);
void protocol_session_unbind(void);
void protocol_session_set_ssl(ProtocolSession* session, SSL* ssl);
void protocol_session_set_bwlimit(ProtocolSession* session, unsigned long long bytes_per_sec);
bool protocol_send_n_data(ProtocolSession* session, const void* data, size_t data_size);
bool protocol_receive_n_data(ProtocolSession* session, void* data, size_t data_size);
bool protocol_send_str(ProtocolSession* session, const char* data);
char* protocol_receive_str(ProtocolSession* session);
bool protocol_send_data(ProtocolSession* session, const Data* data);
Data* protocol_receive_data(ProtocolSession* session);
bool protocol_send_int(ProtocolSession* session, int data);
bool protocol_receive_int(ProtocolSession* session, int* data);
bool protocol_send_status(ProtocolSession* session, Status status);
bool protocol_receive_status(ProtocolSession* session, Status* status);
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);
bool receive_n_data(int file_descriptor, void* data, size_t data_size); bool receive_n_data(int file_descriptor, void* data, size_t data_size);
+154 -148
View File
@@ -1,148 +1,154 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <limits.h>
#include <stdlib.h> #include <stdio.h>
#include <string.h> #include <stdlib.h>
#include <threads.h> #include <string.h>
#include <threads.h>
#include "queue.h"
#include "queue.h"
Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
Queue* queue = (Queue*)malloc(sizeof(Queue)); Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
if (queue == NULL) { if (capacity <= 0)
perror("ERROR: Could not allocate memory for queue structure"); return NULL;
return NULL;
} Queue* queue = (Queue*)malloc(sizeof(Queue));
if (queue == NULL) {
queue->items = malloc(capacity * sizeof(void*)); perror("ERROR: Could not allocate memory for queue structure");
if (queue->items == NULL) { return NULL;
free(queue); }
return NULL;
} queue->items = malloc(capacity * sizeof(void*));
if (queue->items == NULL) {
for (int i = 0; i < capacity; ++i) { free(queue);
queue->items[i] = NULL; return NULL;
} }
queue->capacity = capacity; for (int i = 0; i < capacity; ++i) {
queue->front = 0; queue->items[i] = NULL;
queue->rear = 0; }
queue->size = 0;
queue->item_destroyer = destroyer; queue->capacity = capacity;
queue->front = 0;
return queue; queue->rear = 0;
} queue->size = 0;
queue->item_destroyer = destroyer;
void queue_destroy(Queue* queue) {
if (queue == NULL) return queue;
return; }
if (queue->item_destroyer != NULL) { void queue_destroy(Queue* queue) {
for (int i = 0; i < queue->size; ++i) { if (queue == NULL)
int index = (queue->front + i) % queue->capacity; return;
queue->item_destroyer(queue->items[index]);
} if (queue->item_destroyer != NULL) {
} for (int i = 0; i < queue->size; ++i) {
free(queue->items); int index = (queue->front + i) % queue->capacity;
free(queue); queue->item_destroyer(queue->items[index]);
} }
}
bool queue_is_empty(const Queue* queue) { free(queue->items);
if (queue == NULL) free(queue);
return true; }
return queue->size == 0;
} bool queue_is_empty(const Queue* queue) {
if (queue == NULL)
bool queue_is_full(const Queue* queue) { return true;
if (queue == NULL) return queue->size == 0;
return false; }
return queue->size == queue->capacity;
} bool queue_is_full(const Queue* queue) {
if (queue == NULL)
static bool queue_double_capacity(Queue* queue) { return false;
if (queue == NULL) return queue->size == queue->capacity;
return false; }
unsigned int new_capacity = queue->capacity * 2;
if (new_capacity <= 1) static bool queue_double_capacity(Queue* queue) {
new_capacity = 100; if (queue == NULL)
void** new_items = malloc(new_capacity * sizeof(void*)); return false;
if (new_items == NULL) { if (queue->capacity > INT_MAX / 2)
perror("ERROR: Could not allocate memory for doubling capacity of queue."); return false;
return false; int new_capacity = queue->capacity * 2;
} if (new_capacity <= 1)
for (int i = 0; i < queue->size; i++) new_capacity = 100;
new_items[i] = queue->items[(i + queue->front) % queue->capacity]; void** new_items = malloc(new_capacity * sizeof(void*));
free(queue->items); if (new_items == NULL) {
queue->items = new_items; perror("ERROR: Could not allocate memory for doubling capacity of queue.");
queue->front = 0; return false;
queue->rear = queue->size; }
queue->capacity = new_capacity; for (int i = 0; i < queue->size; i++)
return true; new_items[i] = queue->items[(i + queue->front) % queue->capacity];
} free(queue->items);
queue->items = new_items;
bool queue_enqueue(Queue* queue, void* item) { queue->front = 0;
if (queue == NULL || item == NULL) queue->rear = queue->size;
return false; queue->capacity = new_capacity;
if (queue_is_full(queue)) { return true;
if (!queue_double_capacity(queue)) }
return false;
} bool queue_enqueue(Queue* queue, void* item) {
queue->items[queue->rear] = item; if (queue == NULL || item == NULL)
queue->rear = (queue->rear + 1) % queue->capacity; return false;
queue->size++; if (queue_is_full(queue)) {
return true; if (!queue_double_capacity(queue))
} return false;
}
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty, queue->items[queue->rear] = item;
cnd_t* condition_not_full) { queue->rear = (queue->rear + 1) % queue->capacity;
mtx_lock(mutex); queue->size++;
while (queue_is_full(queue)) return true;
cnd_wait(condition_not_full, mutex); }
bool ok = queue_enqueue(queue, item);
cnd_signal(condition_not_empty); bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
mtx_unlock(mutex); cnd_t* condition_not_full) {
return ok; mtx_lock(mutex);
} while (queue_is_full(queue))
cnd_wait(condition_not_full, mutex);
bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex, bool ok = queue_enqueue(queue, item);
cnd_t* condition_not_empty, cnd_t* condition_not_full, cnd_signal(condition_not_empty);
const bool* cancelled) { mtx_unlock(mutex);
mtx_lock(mutex); return ok;
while (queue_is_full(queue) && (cancelled == NULL || !*cancelled)) }
cnd_wait(condition_not_full, mutex);
if (cancelled != NULL && *cancelled) { bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex,
mtx_unlock(mutex); cnd_t* condition_not_empty, cnd_t* condition_not_full,
return false; const atomic_bool* cancelled) {
} mtx_lock(mutex);
bool ok = queue_enqueue(queue, item); while (queue_is_full(queue) && (cancelled == NULL || !atomic_load(cancelled)))
cnd_signal(condition_not_empty); cnd_wait(condition_not_full, mutex);
mtx_unlock(mutex); if (cancelled != NULL && atomic_load(cancelled)) {
return ok; mtx_unlock(mutex);
} return false;
}
void* queue_dequeue(Queue* queue) { bool ok = queue_enqueue(queue, item);
if (queue == NULL || queue_is_empty(queue)) { cnd_signal(condition_not_empty);
perror("ERROR: Could not dequeue from null or empty queue."); mtx_unlock(mutex);
return NULL; return ok;
} }
void* item = queue->items[queue->front]; void* queue_dequeue(Queue* queue) {
queue->items[queue->front] = NULL; if (queue == NULL || queue_is_empty(queue)) {
queue->front = (queue->front + 1) % queue->capacity; perror("ERROR: Could not dequeue from null or empty queue.");
queue->size--; return NULL;
return item; }
}
void* item = queue->items[queue->front];
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty, queue->items[queue->front] = NULL;
cnd_t* condition_not_full, const bool* other_thread_done) { queue->front = (queue->front + 1) % queue->capacity;
mtx_lock(mutex); queue->size--;
while (queue_is_empty(queue) && !*other_thread_done) return item;
cnd_wait(condition_not_empty, mutex); }
if (queue_is_empty(queue) && *other_thread_done) {
mtx_unlock(mutex); void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
return NULL; cnd_t* condition_not_full, const bool* other_thread_done) {
} mtx_lock(mutex);
void* item = queue_dequeue(queue); while (queue_is_empty(queue) && !*other_thread_done)
cnd_signal(condition_not_full); cnd_wait(condition_not_empty, mutex);
mtx_unlock(mutex); if (queue_is_empty(queue) && *other_thread_done) {
return item; mtx_unlock(mutex);
} return NULL;
}
void* item = queue_dequeue(queue);
cnd_signal(condition_not_full);
mtx_unlock(mutex);
return item;
}
+31 -30
View File
@@ -1,30 +1,31 @@
#ifndef QUEUE_H #ifndef QUEUE_H
#define QUEUE_H #define QUEUE_H
#include <stdbool.h> #include <stdbool.h>
#include <threads.h> #include <stdatomic.h>
#include <threads.h>
typedef struct Queue {
void** items; typedef struct Queue {
int front; void** items;
int rear; int front;
int size; int rear;
int capacity; int size;
void (*item_destroyer)(void* item); int capacity;
} Queue; void (*item_destroyer)(void* item);
} Queue;
Queue* queue_create(int capacity, void (*destroyer)(void* item));
void queue_destroy(Queue* queue); Queue* queue_create(int capacity, void (*destroyer)(void* item));
bool queue_is_empty(const Queue* queue); void queue_destroy(Queue* queue);
bool queue_is_full(const Queue* queue); bool queue_is_empty(const Queue* queue);
bool queue_enqueue(Queue* queue, void* item); bool queue_is_full(const Queue* queue);
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty, bool queue_enqueue(Queue* queue, void* item);
cnd_t* condition_not_full); bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_full);
cnd_t* condition_not_empty, cnd_t* condition_not_full, bool queue_enqueue_multithreaded_cancel(Queue* queue, void* item, mtx_t* mutex,
const bool* cancelled); cnd_t* condition_not_empty, cnd_t* condition_not_full,
void* queue_dequeue(Queue* queue); const atomic_bool* cancelled);
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty, void* queue_dequeue(Queue* queue);
cnd_t* condition_not_full, const bool* other_thread_done); void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
cnd_t* condition_not_full, const bool* other_thread_done);
#endif
#endif
+10 -1
View File
@@ -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);
@@ -170,7 +173,7 @@ Client* client_create() {
return client; return client;
} }
bool client_connect(Client* client, char* host, int port) { bool tcp_connect_socket(Client* client, char* host, int port) {
struct addrinfo hints; struct addrinfo hints;
struct addrinfo* result; struct addrinfo* result;
memset(&hints, 0, sizeof(hints)); memset(&hints, 0, sizeof(hints));
@@ -219,6 +222,12 @@ bool client_connect(Client* client, char* host, int port) {
return false; return false;
} }
return true;
}
bool client_connect(Client* client, char* host, int port) {
if (!tcp_connect_socket(client, host, port))
return false;
tcp_apply_socket_timeout(client->file_descriptor); tcp_apply_socket_timeout(client->file_descriptor);
return true; return true;
} }
+1
View File
@@ -30,6 +30,7 @@ void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
void server_delete(Server** server); void server_delete(Server** server);
Client* client_create(); Client* client_create();
bool client_connect(Client* client, char* host, int port); bool client_connect(Client* client, char* host, int port);
bool tcp_connect_socket(Client* client, char* host, int port);
void client_disconnect(Client* client); void client_disconnect(Client* client);
void client_delete(Client* client); void client_delete(Client* client);
void tcp_set_timeouts(int timeout_sec, int contimeout_sec); void tcp_set_timeouts(int timeout_sec, int contimeout_sec);
+2 -50
View File
@@ -3,15 +3,12 @@
#include "protocol.h" #include "protocol.h"
#include "transport_tcp.h" #include "transport_tcp.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <netdb.h>
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <signal.h> #include <signal.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
bool tls_global_init(void) { bool tls_global_init(void) {
@@ -71,7 +68,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);
@@ -149,53 +146,8 @@ bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
bool client_connect_tls(Client* client, char* host, int port, const char* cert_path, bool client_connect_tls(Client* client, char* host, int port, const char* cert_path,
const char* key_path, const char* ca_path) { const char* key_path, const char* ca_path) {
struct addrinfo hints; if (!tcp_connect_socket(client, host, port))
struct addrinfo* result;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
char port_str[16];
snprintf(port_str, sizeof(port_str), "%d", port);
int err = getaddrinfo(host, port_str, &hints, &result);
if (err != 0 || result == NULL) {
fprintf(stderr, "Could not resolve host: %s (%s)\n", host, gai_strerror(err));
return false; return false;
}
struct addrinfo* rp;
bool connected = false;
for (rp = result; rp != NULL; rp = rp->ai_next) {
if (client->file_descriptor >= 0)
close(client->file_descriptor);
client->file_descriptor = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (client->file_descriptor < 0)
continue;
struct timeval ct;
ct.tv_sec = tcp_get_contimeout_sec();
ct.tv_usec = 0;
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct));
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct));
memcpy(&client->address, rp->ai_addr, rp->ai_addrlen);
client->address_length = rp->ai_addrlen;
if (connect(client->file_descriptor, (struct sockaddr*)&client->address,
client->address_length) == 0) {
connected = true;
break;
}
}
freeaddrinfo(result);
if (!connected) {
perror("Could not connect to Server!");
return false;
}
SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path); SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path);
if (!ctx) if (!ctx)
+46 -21
View File
@@ -3,12 +3,19 @@
#include "libgen.h" #include "libgen.h"
#include <dirent.h> #include <dirent.h>
#include <errno.h> #include <errno.h>
#include <fcntl.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
static int authorized_root_fd = -1;
void utils_set_authorized_root_fd(int fd) {
authorized_root_fd = fd;
}
bool mkdir_r(const char* path) { bool mkdir_r(const char* path) {
size_t path_len = strlen(path); size_t path_len = strlen(path);
char* path_duplicate = malloc(path_len + 1); char* path_duplicate = malloc(path_len + 1);
@@ -136,34 +143,43 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
return false; return false;
} }
static void delete_extras_walk(const char* abs_path, const char* rel_path, ArrayList* manifest) { static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest) {
DIR* dir = opendir(abs_path); int scanfd = dup(dirfd);
if (!dir) if (scanfd < 0)
return; return false;
DIR* dir = fdopendir(scanfd);
if (!dir) {
close(scanfd);
return false;
}
bool all_removed = true; bool all_removed = true;
bool operation_ok = true;
const struct dirent* entry; const struct dirent* entry;
while ((entry = readdir(dir)) != NULL) { while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue; continue;
char* child_abs = path_cat((char*)abs_path, entry->d_name);
char* child_rel = path_cat((char*)rel_path, entry->d_name); char* child_rel = path_cat((char*)rel_path, entry->d_name);
struct stat st; struct stat st;
if (lstat(child_abs, &st) != 0) { if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
free(child_abs);
free(child_rel); free(child_rel);
continue; continue;
} }
// Skip symlinks to prevent following them outside the destination tree // Skip symlinks to prevent following them outside the destination tree
if (S_ISLNK(st.st_mode)) { if (S_ISLNK(st.st_mode)) {
free(child_abs);
free(child_rel); free(child_rel);
continue; continue;
} }
if (S_ISDIR(st.st_mode)) { if (S_ISDIR(st.st_mode)) {
delete_extras_walk(child_abs, child_rel, manifest); int childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
// After recursion, try to remove the subdirectory if it's now empty. bool child_removed = false;
// Ignore ENOENT: the recursive call may have already removed it. if (childfd >= 0) {
if (rmdir(child_abs) != 0 && errno != ENOENT) { child_removed = delete_extras_fd(childfd, child_rel, manifest);
close(childfd);
}
if (child_removed && !is_dir_in_manifest(child_rel, manifest) &&
unlinkat(dirfd, entry->d_name, AT_REMOVEDIR) != 0 && errno != ENOENT) {
operation_ok = false;
} else if (!child_removed) {
all_removed = false; all_removed = false;
} }
} else { } else {
@@ -176,25 +192,30 @@ static void delete_extras_walk(const char* abs_path, const char* rel_path, Array
} }
} }
if (!found) { if (!found) {
unlink(child_abs); if (unlinkat(dirfd, entry->d_name, 0) != 0 && errno != ENOENT)
operation_ok = false;
fprintf(stderr, " Deleted: %s\n", child_rel); fprintf(stderr, " Deleted: %s\n", child_rel);
} else { } else {
all_removed = false; all_removed = false;
} }
} }
free(child_abs);
free(child_rel); free(child_rel);
} }
closedir(dir); closedir(dir);
// Only remove the directory itself if it is not in the manifest (void)all_removed;
// and contained no kept entries. return operation_ok;
if (all_removed && rel_path[0] != '\0' && !is_dir_in_manifest(rel_path, manifest)) {
rmdir(abs_path);
}
} }
void delete_extras(const char* dest_root, ArrayList* manifest) { bool delete_extras(const char* dest_root, ArrayList* manifest) {
delete_extras_walk(dest_root, "", manifest); int rootfd = authorized_root_fd >= 0
? dup(authorized_root_fd)
: open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (rootfd < 0)
return false;
bool ok = delete_extras_fd(rootfd, "", manifest);
if (close(rootfd) != 0)
ok = false;
return ok;
} }
bool has_path_traversal(const char* path) { bool has_path_traversal(const char* path) {
@@ -216,6 +237,10 @@ bool has_path_traversal(const char* path) {
return false; return false;
} }
bool utils_valid_batch_path(const char* path) {
return path && path[0] != '\0' && path[0] != '/' && !has_path_traversal(path);
}
char* path_cat(const char* path1, const char* path2) { char* path_cat(const char* path1, const char* path2) {
if (path1 == NULL || *path1 == '\0') if (path1 == NULL || *path1 == '\0')
return str_dup(path2); return str_dup(path2);
+3 -1
View File
@@ -8,7 +8,9 @@ bool mkdir_r(const char* path);
char* str_dup(const char* string); char* str_dup(const char* string);
char* path_cat(const char* path1, const char* path2); char* path_cat(const char* path1, const char* path2);
bool glob_match(const char* pattern, const char* str); bool glob_match(const char* pattern, const char* str);
void delete_extras(const char* dest_root, ArrayList* manifest); bool delete_extras(const char* dest_root, ArrayList* manifest);
void utils_set_authorized_root_fd(int fd);
bool has_path_traversal(const char* path); bool has_path_traversal(const char* path);
bool utils_valid_batch_path(const char* path);
#endif #endif
+26 -3
View File
@@ -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):
@@ -220,8 +240,10 @@ class TestDelete:
) )
assert result.returncode == 0, f"Delete sync failed: {(result.stderr or result.stdout)[:200]}" assert result.returncode == 0, f"Delete sync failed: {(result.stderr or result.stdout)[:200]}"
assert not os.path.exists(extra_file), "extra_file.txt should be deleted" # The default server policy intentionally refuses client-requested
assert not os.path.exists(extra_dir), "extra_dir should be deleted" # deletion unless it is started with --allow-delete.
assert os.path.exists(extra_file), "unauthorized delete removed an extra file"
assert os.path.exists(extra_dir), "unauthorized delete removed an extra directory"
mismatches, missing = verify_transfer(SOURCE_DIR, received) mismatches, missing = verify_transfer(SOURCE_DIR, received)
assert not missing, f"Missing: {missing}" assert not missing, f"Missing: {missing}"
@@ -238,7 +260,8 @@ class TestProgress:
) )
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}" assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}"
output = result.stdout + result.stderr output = result.stdout + result.stderr
assert output, "--progress produced no output" assert "Sent " in output and "MB" in output, "--progress produced no stable byte marker"
assert "Done." in output, "--progress did not report completion"
class TestBandwidthLimit: class TestBandwidthLimit:
+40 -43
View File
@@ -4,52 +4,51 @@ import shutil
import subprocess import subprocess
import sys import sys
import pytest import pytest
import shlex
import tempfile
import shutil
sys.path.insert(0, os.path.dirname(__file__)) sys.path.insert(0, os.path.dirname(__file__))
from common import ( from common import (PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR, CLIENT_CMD,
PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR, generate_test_files, verify_transfer, clean_dir, make_result,
CLIENT_CMD, generate_test_files, verify_transfer, clean_dir, make_result, get_dest_received_dir)
)
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "ssh_source") SOURCE_DIR = os.path.join(TEST_DATA_DIR, "ssh_source")
DEST_DIR = os.path.join(TEST_DATA_DIR, "ssh_dest") DEST_DIR = os.path.join(TEST_DATA_DIR, "ssh_dest")
SSH_AVAILABLE = False SSH_AVAILABLE = False
SSH_SKIP_REASON = "SSH localhost probe was not run"
SSH_PROBE_DIR = None
def _check_ssh(): def _check_ssh():
global SSH_AVAILABLE global SSH_AVAILABLE, SSH_SKIP_REASON, SSH_PROBE_DIR
server_path = os.path.join(BUILD_DIR, "server")
if not os.path.isfile(server_path):
SSH_SKIP_REASON = f"current server binary is missing: {server_path}"
return
try: try:
r = subprocess.run( SSH_PROBE_DIR = tempfile.mkdtemp(prefix="fastsync-ssh-probe-")
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", probe_server = os.path.join(SSH_PROBE_DIR, "fastsync-server")
"localhost", "which", "fastsync-server"], os.symlink(server_path, probe_server)
capture_output=True, timeout=10, command = f"{shlex.quote(probe_server)} --help"
) path = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
if r.returncode == 0: "localhost", "sh", "-c", command],
capture_output=True, timeout=10, text=True)
if path.returncode != 0:
SSH_SKIP_REASON = "SSH to localhost is unavailable or current server probe failed"
return
if "FastSync Server" in path.stdout:
SSH_AVAILABLE = True SSH_AVAILABLE = True
return return
SSH_SKIP_REASON = "SSH probe did not execute the current server binary"
# Try to install server binary into PATH
server_path = os.path.join(BUILD_DIR, "server")
r = subprocess.run(
["ssh", "-o", "BatchMode=yes", "localhost", 'echo "$PATH"'],
capture_output=True, timeout=10, text=True,
)
if r.returncode != 0:
return
for d in r.stdout.strip().split(":"):
d = d.strip()
if not d or "wrappers" in d:
continue
test = subprocess.run(
["ssh", "-o", "BatchMode=yes", "localhost",
f'test -w "{d}" && ln -sf {server_path} "{d}/fastsync-server" && which fastsync-server'],
capture_output=True, timeout=10,
)
if test.returncode == 0:
SSH_AVAILABLE = True
return
except FileNotFoundError: except FileNotFoundError:
pass SSH_SKIP_REASON = "ssh executable is unavailable"
except (OSError, subprocess.TimeoutExpired) as exc:
SSH_SKIP_REASON = f"SSH setup failed: {exc}"
finally:
if SSH_PROBE_DIR:
shutil.rmtree(SSH_PROBE_DIR, ignore_errors=True)
SSH_PROBE_DIR = None
_check_ssh() _check_ssh()
@@ -65,18 +64,17 @@ def setup_test_data():
def _run_ssh_test(name, flags, expected_missing=None): def _run_ssh_test(name, flags, expected_missing=None):
"""Run an SSH test case (no server process needed, client spawns SSH)."""
ssh_dest = f"localhost:{DEST_DIR}" ssh_dest = f"localhost:{DEST_DIR}"
clean_dir(DEST_DIR) clean_dir(DEST_DIR)
cmd = CLIENT_CMD + [SOURCE_DIR, ssh_dest, "--save-to-disk"] + flags cmd = CLIENT_CMD + [SOURCE_DIR, ssh_dest, "--save-to-disk",
"--fastsync-server-path", os.path.join(BUILD_DIR, "server")] + flags
start = __import__("time").monotonic() start = __import__("time").monotonic()
result = subprocess.run(cmd, text=True, capture_output=True) result = subprocess.run(cmd, text=True, capture_output=True)
duration = __import__("time").monotonic() - start duration = __import__("time").monotonic() - start
if result.returncode != 0: if result.returncode != 0:
return make_result(name, False, duration, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:100]}") return make_result(name, False, duration,
f"Exit {result.returncode}: {(result.stderr or result.stdout)[:100]}")
mismatches, missing = verify_transfer(SOURCE_DIR, DEST_DIR) mismatches, missing = verify_transfer(SOURCE_DIR, get_dest_received_dir(DEST_DIR, SOURCE_DIR))
if expected_missing: if expected_missing:
missing = [m for m in missing if m not in expected_missing] missing = [m for m in missing if m not in expected_missing]
if missing: if missing:
@@ -90,7 +88,7 @@ class TestSSHStandard:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def require_ssh(self): def require_ssh(self):
if not SSH_AVAILABLE: if not SSH_AVAILABLE:
pytest.skip("SSH to localhost not available") pytest.skip(SSH_SKIP_REASON)
def test_standard(self): def test_standard(self):
r = _run_ssh_test("SSH (localhost)", []) r = _run_ssh_test("SSH (localhost)", [])
@@ -129,7 +127,7 @@ class TestSSHFeatures:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def require_ssh(self): def require_ssh(self):
if not SSH_AVAILABLE: if not SSH_AVAILABLE:
pytest.skip("SSH to localhost not available") pytest.skip(SSH_SKIP_REASON)
def test_archive(self): def test_archive(self):
r = _run_ssh_test("SSH Archive (-a)", ["-a"]) r = _run_ssh_test("SSH Archive (-a)", ["-a"])
@@ -137,6 +135,5 @@ class TestSSHFeatures:
def test_exclude(self): def test_exclude(self):
r = _run_ssh_test("SSH Exclude (--exclude small.txt)", r = _run_ssh_test("SSH Exclude (--exclude small.txt)",
["--exclude", "small.txt"], ["--exclude", "small.txt"], expected_missing=["small.txt"])
expected_missing=["small.txt"])
assert r["status"] == "Success", r["error"] assert r["status"] == "Success", r["error"]
+2
View File
@@ -25,6 +25,7 @@
#include "test_transport_tls.h" #include "test_transport_tls.h"
#include "test_utils.h" #include "test_utils.h"
#include <stdio.h> #include <stdio.h>
#include <signal.h>
// Define global test state variables // Define global test state variables
int tests_run = 0; int tests_run = 0;
@@ -32,6 +33,7 @@ int tests_failed = 0;
bool current_test_failed = false; bool current_test_failed = false;
int main() { int main() {
signal(SIGPIPE, SIG_IGN);
printf("\033[1;36m=== RUNNING UNIT TESTS ===\033[0m\n\n"); printf("\033[1;36m=== RUNNING UNIT TESTS ===\033[0m\n\n");
RUN_TEST(test_queue); RUN_TEST(test_queue);
+119
View File
@@ -1,4 +1,5 @@
#include "test_client_cli.h" #include "test_client_cli.h"
#include "client_validation.h"
#include "config.h" #include "config.h"
#include "test_utils.h" #include "test_utils.h"
#include "utils.h" #include "utils.h"
@@ -9,6 +10,56 @@
/* Declaration of parse_args from client_cli.c */ /* Declaration of parse_args from client_cli.c */
int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count); int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count);
static Config* valid_client_config() {
Config* cfg = config_create();
if (!cfg)
return NULL;
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
return cfg;
}
static void test_validate_config_required_paths() {
Config* cfg = config_create();
EXPECT_FALSE(validate_config(cfg));
cfg->send_directory = str_dup("/src");
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
static void test_validate_config_incompatible_options() {
Config* cfg = valid_client_config();
cfg->use_sendfile = true;
cfg->use_compression = true;
EXPECT_FALSE(validate_config(cfg));
cfg->use_compression = false;
cfg->use_incremental = true;
cfg->use_chunk_serialization = true;
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
static void test_validate_config_tls_requirements() {
Config* cfg = valid_client_config();
cfg->use_tls = true;
EXPECT_FALSE(validate_config(cfg));
cfg->tls_cert = str_dup("cert.pem");
EXPECT_FALSE(validate_config(cfg));
cfg->tls_key = str_dup("key.pem");
EXPECT_TRUE(validate_config(cfg));
config_delete(cfg);
}
static void test_validate_config_delta_sendfile_constraints() {
Config* cfg = valid_client_config();
cfg->use_delta = true;
EXPECT_FALSE(validate_config(cfg));
cfg->use_incremental = true;
cfg->use_sendfile = true;
EXPECT_FALSE(validate_config(cfg));
config_delete(cfg);
}
/* Test main() with --help flag (early return path, no server connection needed) */ /* Test main() with --help flag (early return path, no server connection needed) */
static void test_cli_help() { static void test_cli_help() {
/* We can't easily call main() because it calls send_files which needs a server. /* We can't easily call main() because it calls send_files which needs a server.
@@ -207,6 +258,69 @@ static void test_parse_args_unknown_option() {
config_delete(cfg); config_delete(cfg);
} }
/* Parsed-but-unimplemented options must fail instead of being silently accepted. */
static void test_parse_args_rejects_unimplemented_options() {
static const char* const options[] = {"-q",
"--quiet",
"--silent",
"--queue-size",
"-H",
"--hard-links",
"-A",
"--acls",
"-X",
"--xattrs",
"-D",
"--devices",
"-i",
"--itemize-changes",
"--out-format",
"--info",
"--debug",
"--list-only",
"-h",
"--human-readable",
"-u",
"--update",
"--append",
"--append-verify",
"--delete-excluded",
"--delete-after",
"--max-delete",
"--filter",
"--files-from",
"--cvs-exclude",
"--prune-empty-dirs",
"-R",
"--relative",
"-e",
"--rsh",
"--rsync-path",
"--temp-dir",
"--compare-dest",
"--copy-dest",
"--link-dest",
"--delete-before",
"--address",
"--bind-address",
"--ipv6",
"--ipv4",
"--daemon",
"--config",
"--server",
"--compress-choice"};
for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) {
Config* cfg = config_create();
char* argv[] = {"fastsync", (char*)options[i], "dummy", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
config_delete(cfg);
}
}
/* Test parse_args with --archive flag */ /* Test parse_args with --archive flag */
static void test_parse_args_archive() { static void test_parse_args_archive() {
Config* cfg = config_create(); Config* cfg = config_create();
@@ -224,6 +338,10 @@ static void test_parse_args_archive() {
} }
void test_client_cli() { void test_client_cli() {
test_validate_config_required_paths();
test_validate_config_incompatible_options();
test_validate_config_tls_requirements();
test_validate_config_delta_sendfile_constraints();
test_cli_help(); test_cli_help();
test_cli_archive_flags(); test_cli_archive_flags();
test_cli_dry_run(); test_cli_dry_run();
@@ -238,5 +356,6 @@ void test_client_cli() {
test_parse_args_invalid_compression_level(); test_parse_args_invalid_compression_level();
test_parse_args_valid_compression_level(); test_parse_args_valid_compression_level();
test_parse_args_unknown_option(); test_parse_args_unknown_option();
test_parse_args_rejects_unimplemented_options();
test_parse_args_archive(); test_parse_args_archive();
} }
+21
View File
@@ -224,6 +224,26 @@ static void test_config_send_receive_version_mismatch() {
} }
} }
static void test_config_receive_truncated() {
int p[2];
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
io_set_fds(p[0], p[0]);
io_set_bwlimit(0);
/* A valid prefix exercises cleanup after allocated wire strings and a
* partially received scalar field. */
EXPECT_TRUE(send_str(p[1], PROTOCOL_VERSION));
EXPECT_TRUE(send_str(p[1], "/src"));
EXPECT_TRUE(send_str(p[1], "/dst"));
EXPECT_TRUE(send_int(p[1], 1));
shutdown(p[1], SHUT_WR);
const Config* cfg = config_receive(p[0]);
EXPECT_NULL(cfg);
close(p[0]);
close(p[1]);
}
static void test_is_remote_dest() { static void test_is_remote_dest() {
/* Valid SSH-style destinations */ /* Valid SSH-style destinations */
EXPECT_TRUE(is_remote_dest("user@host:/path")); EXPECT_TRUE(is_remote_dest("user@host:/path"));
@@ -256,6 +276,7 @@ void test_config() {
if (!is_running_under_valgrind()) { if (!is_running_under_valgrind()) {
test_config_send_receive(); test_config_send_receive();
test_config_send_receive_version_mismatch(); test_config_send_receive_version_mismatch();
test_config_receive_truncated();
} }
test_is_remote_dest(); test_is_remote_dest();
} }
+5
View File
@@ -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++)
@@ -344,6 +348,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();
+24
View File
@@ -126,6 +126,29 @@ 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);
// cppcheck-suppress knownConditionTrueFalse
if (!fp)
return;
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 +457,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();
+14
View File
@@ -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();
} }
+44 -8
View File
@@ -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);
@@ -91,13 +91,13 @@ static void test_sender_zero_capacity() {
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");
Queue* q1 = queue_create(0, NULL); // cppcheck-suppress constVariablePointer
Queue* q2 = queue_create(0, NULL); Queue* const q1 = queue_create(0, NULL);
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2); // cppcheck-suppress constVariablePointer
EXPECT_NOT_NULL(ctx); Queue* const q2 = queue_create(0, NULL);
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 0); EXPECT_NULL(q1);
EXPECT_EQ_INT(ctx->queue_loader->capacity, 0); EXPECT_NULL(q2);
pipeline_context_sender_destroy(ctx); config_delete(cfg);
} }
/* Test receiver with zero file_descriptor */ /* Test receiver with zero file_descriptor */
@@ -168,6 +168,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 +258,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();
} }
+18
View File
@@ -38,6 +38,23 @@ static void test_send_receive_n_data_zero() {
close(p[1]); close(p[1]);
} }
static void test_explicit_session_context() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
ProtocolSession session;
protocol_session_init(&session, p[0], p[1]);
protocol_session_set_bwlimit(&session, 0);
const char payload[] = "explicit context";
char received[sizeof(payload)] = {0};
EXPECT_TRUE(protocol_send_n_data(&session, payload, sizeof(payload)));
EXPECT_TRUE(protocol_receive_n_data(&session, received, sizeof(received)));
EXPECT_EQ_INT(memcmp(payload, received, sizeof(payload)), 0);
close(p[0]);
close(p[1]);
}
static void test_send_receive_str() { static void test_send_receive_str() {
int p[2]; int p[2];
EXPECT_EQ_INT(pipe(p), 0); EXPECT_EQ_INT(pipe(p), 0);
@@ -173,6 +190,7 @@ static void test_receive_str_truncated() {
void test_protocol() { void test_protocol() {
test_send_receive_n_data(); test_send_receive_n_data();
test_send_receive_n_data_zero(); test_send_receive_n_data_zero();
test_explicit_session_context();
test_send_receive_str(); test_send_receive_str();
test_send_receive_str_normal(); test_send_receive_str_normal();
test_send_receive_data(); test_send_receive_data();
+6
View File
@@ -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();
+20 -8
View File
@@ -11,11 +11,7 @@
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
/* Include server.c but rename main to avoid conflict with test runner's main */ #include "receiver.h"
#define main server_main_
#define FASTSYNC_SERVER_AS_LIB
#include "server.c"
#undef main
/* Test receive_files with immediate FINISHED status */ /* Test receive_files with immediate FINISHED status */
static void test_receive_files_finished() { static void test_receive_files_finished() {
@@ -36,7 +32,7 @@ static void test_receive_files_finished() {
/* Child: use p[0] for both read and write */ /* Child: use p[0] for both read and write */
close(p[1]); close(p[1]);
io_set_fds(p[0], p[0]); io_set_fds(p[0], p[0]);
int ret = receive_files(cfg, p[0]); int ret = receiver_receive_files(cfg, p[0]);
close(p[0]); close(p[0]);
config_delete(cfg); config_delete(cfg);
_exit(ret == 0 ? 0 : 1); _exit(ret == 0 ? 0 : 1);
@@ -88,7 +84,7 @@ static void test_receive_files_single_file() {
/* Child: use p[0] for both read and write */ /* Child: use p[0] for both read and write */
close(p[1]); close(p[1]);
io_set_fds(p[0], p[0]); io_set_fds(p[0], p[0]);
int ret = receive_files(cfg, p[0]); int ret = receiver_receive_files(cfg, p[0]);
close(p[0]); close(p[0]);
config_delete(cfg); config_delete(cfg);
_exit(ret == 0 ? 0 : 1); _exit(ret == 0 ? 0 : 1);
@@ -149,7 +145,7 @@ static void test_receive_files_abort() {
if (pid == 0) { if (pid == 0) {
close(p[1]); close(p[1]);
io_set_fds(p[0], p[0]); io_set_fds(p[0], p[0]);
int ret = receive_files(cfg, p[0]); int ret = receiver_receive_files(cfg, p[0]);
close(p[0]); close(p[0]);
config_delete(cfg); config_delete(cfg);
/* Should return -1 on abort */ /* Should return -1 on abort */
@@ -171,10 +167,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();
} }
} }