Compare commits

..

1 Commits

Author SHA1 Message Date
TapTap 246759776b fix: features and enhancements — issues #70, #36, #34, #33, #32, #57, #40, #37
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-07-20 19:27:13 +02:00
50 changed files with 1011 additions and 2461 deletions
-3
View File
@@ -5,6 +5,3 @@ __pycache__/
build-asan build-asan
coverage.info coverage.info
build-*/ build-*/
build2/
build3/
build_docker2/
-87
View File
@@ -72,90 +72,3 @@ git push -u origin <feature-branch-name>
gh pr create --fill gh pr create --fill
``` ```
Wait for CI to pass on the PR before merging. Wait for CI to pass on the PR before merging.
## Batch PR Workflow
When handling multiple issues split across several PRs that target the same files:
1. **Group issues by logical category** into separate PR branches (e.g., memory-safety, refactoring, test-coverage).
2. **Fix and push** each branch independently. Let CI run on each PR.
3. **Run all 3 reviewer types** on each PR and post results to Gitea via `tea pr approve/reject` or the Gitea API:
- `reviewer` — general code correctness
- `code-quality-guardian` — code quality, duplication, complexity
- `security-auditor` — vulnerability assessment
4. **Iterate**: if any reviewer requests changes, fix, push, re-review. Repeat until all 3 approve.
5. **Merge approved PRs** one at a time into `main`.
6. **Create a combined merge branch** for the remaining PRs that conflict with the new `main`:
```bash
git checkout -b merge-all origin/main
for branch in branch1 branch2 branch3; do
git merge origin/$branch --no-edit || true
# Resolve conflicts, build, test
done
```
7. **Run review again** on the combined branch. Fix issues, push, re-review until approved.
8. **Merge** the combined PR, **close** the redundant individual PRs, and **close all resolved issues** via the Gitea API:
```bash
curl -s -X PATCH -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d '{"state":"closed"}' \
"https://gitea.tap-tap.win/api/v1/repos/owner/repo/issues/<number>"
```
## CI Troubleshooting
### If lint (clang-format) fails
Run clang-format in the CI Docker image to match the exact CI version:
```bash
docker run --rm -v "$PWD:/workspace" -w /workspace gitea.tap-tap.win/taptap/fastsync-ci:v9 \
sh -c 'find src/ tests/ -name "*.c" -o -name "*.h" | xargs clang-format -i'
```
### If cppcheck fails
Fix reported issues locally, then verify with:
```bash
docker run --rm -v "$PWD:/workspace" -w /workspace gitea.tap-tap.win/taptap/fastsync-ci:v9 \
sh -c 'cppcheck --enable=warning,style,performance,portability --suppress=missingIncludeSystem --error-exitcode=1 --inline-suppr src/ tests/'
```
### If integration tests fail
Run locally before pushing:
```bash
python3 -m pytest tests/ -v --tb=short
```
## Gitea API & tea CLI
### Check CI status via API
```bash
TOKEN="<token>"
curl -s -H "Authorization: token $TOKEN" \
"https://gitea.tap-tap.win/api/v1/repos/TapTap/FastSync/actions/runs?limit=5" \
| python3 -c "
import json,sys; d=json.load(sys.stdin)
for r in d.get('workflow_runs',[]):
path = r.get('path','')
prn = path.split('@')[1].replace('refs/pull/','').replace('/head','') if '@' in path else ''
print(f'PR #{prn}: sha={r[\"head_sha\"][:8]} {r[\"status\"]} {r.get(\"conclusion\",\"\")}')
"
```
### Post review comments
```bash
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
-d '{"body":"MARKDOWN_REVIEW_BODY"}' \
"https://gitea.tap-tap.win/api/v1/repos/TapTap/FastSync/issues/<PR_NUMBER>/comments"
```
### Use tea for PR operations
```bash
tea pr list --repo TapTap/FastSync
tea pr close <number> --repo TapTap/FastSync
```
## Common pitfalls
- **`__thread` on shared SSL context**: io_ssl must NOT be thread-local — worker threads inherit the SSL context from the main thread. Use regular `static SSL* io_ssl`.
- **SSL WANT_READ/WANT_WRITE retry**: Always retry on `SSL_ERROR_WANT_READ` and `SSL_ERROR_WANT_WRITE` in `send_n_data`/`receive_n_data`. Removing these breaks TLS multithreaded transfers.
- **clang-format version**: The CI image uses clang-format 18. Always format inside the CI Docker container for exact match.
- **Merge order matters**: Merge the most comprehensive branch first, then smaller ones, to minimize conflicts when creating a combined branch.
+19 -110
View File
@@ -5,26 +5,17 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e
## Technical Overview ## Technical Overview
1. **Dual transport**: custom TCP client-server or SSH subprocess (rsync-style `user@host:/path`) 1. **Dual transport**: custom TCP client-server or SSH subprocess (rsync-style `user@host:/path`)
2. **TLS encryption**: OpenSSL-based TLS 1.2+ for encrypted TCP connections with optional CA verification 2. **TLS encryption**: OpenSSL-based TLS 1.2+ for encrypted TCP connections
3. **Chunked file transfer**: files grouped into configurable-size chunks (default ~10 MB) 3. **Chunked file transfer**: files grouped into configurable-size chunks (default ~10 MB)
4. **Streaming zstd compression** (levels 122) using `ZSTD_compressStream2` 4. **Streaming zstd compression** (levels 122) using `ZSTD_compressStream2`
5. **Multithreading**: producer-consumer pipeline with thread-safe queues (scanner → loader → sender) 5. **Multithreading**: producer-consumer pipeline with thread-safe queues (scanner → loader → sender)
6. **Incremental sync**: skip files unchanged since last transfer (compares size + mtime) 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 7. **Metadata preservation**: `mode`, `uid`, `gid`, `mtime` restored on disk when enabled
8. **Metadata preservation**: `mode`, `uid`, `gid`, `mtime` restored on disk when enabled 8. **`sendfile()` zero-copy** on TCP (~2× faster on loopback)
9. **`sendfile()` zero-copy** on TCP (~2× faster on loopback) 9. **SSH ControlMaster** for connection reuse across repeated invocations
10. **SSH ControlMaster** for connection reuse across repeated invocations 10. **Bandwidth limiting**: token-bucket throttling (`--bwlimit`)
11. **Bandwidth limiting**: token-bucket throttling (`--bwlimit`) 11. **`--delete`**: receiver removes files not present in sender manifest
12. **`--delete`**: receiver removes files not present in sender manifest 12. **`--exclude` / `--include`**: glob-pattern filename filtering
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 ## System Architecture
@@ -34,33 +25,20 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e
- Streaming zstd compression with configurable level - Streaming zstd compression with configurable level
- Chunk serialization (compact binary format) or per-file transfer - Chunk serialization (compact binary format) or per-file transfer
- Incremental transfer: sends file metadata to server, skips unchanged files - 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 - Manifests all sent paths when `--delete` is active
- Sends via TCP `sendfile()` or SSH pipe - Sends via TCP `sendfile()` or SSH pipe
- Optional progress display with throughput - Optional progress display with throughput
- Bandwidth limiting via token-bucket algorithm - 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 ### Server
- TCP mode: listens on configurable port (default 8080); SSH mode: runs via `--stdio` - TCP mode: listens on configurable port (default 8080); SSH mode: runs via `--stdio`
- TLS mode: wraps TCP connections with OpenSSL with optional CA verification - TLS mode: wraps TCP connections with OpenSSL
- Receives and reassembles files - Receives and reassembles files
- Decompresses (streaming zstd), deserializes, restores metadata - Decompresses (streaming zstd), deserializes, restores metadata
- Handles incremental checks: compares size + mtime against destination files - 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 - Processes `STATUS_MANIFEST` for `--delete`: walks destination tree, removes extras
- Per-connection concurrency via `fork()` with configurable connection limit (default 100) - Per-connection concurrency via `fork()`
- Thread pool for parallel processing - 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 ## Protocol Details
@@ -74,11 +52,6 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e
| `STATUS_CHUNK` | Following data is a serialized chunk | | `STATUS_CHUNK` | Following data is a serialized chunk |
| `STATUS_MANIFEST` | Following data is a file manifest (for `--delete`) | | `STATUS_MANIFEST` | Following data is a file manifest (for `--delete`) |
| `STATUS_CHECK` | Incremental check: client sends file path + size + mtime, server responds with OK (skip) or NEXT (send) | | `STATUS_CHECK` | Incremental check: client sends file path + size + mtime, 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 ### Wire Format — Metadata
@@ -86,16 +59,12 @@ When `use_metadata` is enabled (`-M`), each file entry carries a 4-byte `present
### Transfer Flow ### Transfer Flow
``` ```
Config → (STATUS_NEXT | STATUS_CHUNK | STATUS_CHECK | STATUS_CHECK_BATCH)* → [STATUS_MANIFEST] → STATUS_FINISHED → STATUS_OK Config → (STATUS_NEXT | STATUS_CHUNK | STATUS_CHECK)* → [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.
Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up temporary files and exits the child process.
### Protocol Version ### Protocol Version
`1.3.0` — server and client must match. Mismatch results in `STATUS_ERROR`. `1.1.0` — server and client must match. Mismatch results in `STATUS_ERROR`.
## Command-Line Arguments ## Command-Line Arguments
@@ -114,26 +83,15 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up
| `-n, --dry-run` | Scan and print what would be transferred | | `-n, --dry-run` | Scan and print what would be transferred |
| `-p <port>` | SSH port (default: 22) | | `-p <port>` | SSH port (default: 22) |
| `-v, --verbose` | Enable debug logging | | `-v, --verbose` | Enable debug logging |
| `-q, --quiet` | Suppress all non-error output |
| `--silent` | Alias for `--quiet` |
| `--progress` | Show real-time transfer speed | | `--progress` | Show real-time transfer speed |
| `--delete` | Delete files on receiver not present in source | | `--delete` | Delete files on receiver not present in source |
| `--exclude <pattern>` | Exclude files matching glob pattern (repeatable) | | `--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) | | `--include <pattern>` | Only transfer files matching glob pattern (repeatable, whitelist) |
| `--max-size <n>` | Skip files larger than n bytes | | `--max-size <n>` | Skip files larger than n bytes |
| `--min-size <n>` | Skip files smaller 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`. | | `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `-s`. |
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second | | `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) | | `--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`) | | `--source-dir <path>` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) |
| `--dest-dir <path>` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) | | `--dest-dir <path>` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) |
| `--save-to-disk` | Write received files to disk | | `--save-to-disk` | Write received files to disk |
@@ -164,12 +122,6 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up
| `FASTSYNC_SOURCE_DIR` | — | Source directory fallback | | `FASTSYNC_SOURCE_DIR` | — | Source directory fallback |
| `FASTSYNC_DEST_DIR` | — | Destination directory fallback | | `FASTSYNC_DEST_DIR` | — | Destination directory fallback |
| `FASTSYNC_SAVE_TO_DISK` | `false` | Disk persistence 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 ## Implementation Details
@@ -177,44 +129,21 @@ Abort (`STATUS_ABORT`) may be sent at any point. On receipt the server cleans up
1. **Chunk** — collection of files (~10 MB total by default) 1. **Chunk** — collection of files (~10 MB total by default)
2. **File** — path, content (`Data`), optional `FileMetadata` pointer 2. **File** — path, content (`Data`), optional `FileMetadata` pointer
3. **FileMetadata**`mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec` 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`. 4. **Config** — runtime parameters (transported over wire, TLS settings excluded)
5. **Queue** — thread-safe bounded queue with condition variables 5. **Queue** — thread-safe bounded queue with condition variables
6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support, max-depth enforcement 6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support
### Key Algorithms ### Key Algorithms
1. **File scanning** — BFS directory traversal; entries matched against exclude and include patterns, max-depth enforced 1. **File scanning** — BFS directory traversal; entries matched against exclude and include patterns
2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed 2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed
3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream` 3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream`
4. **Network protocol** — status-code-driven exchange with metadata packing, keep-alive, and abort support 4. **Network protocol** — status-code-driven exchange with metadata packing
5. **Incremental check** — client sends `STATUS_CHECK` + path + size + mtime; server compares against destination. Can be batched via `STATUS_CHECK_BATCH` for reduced round-trips. 5. **Incremental check** — client sends `STATUS_CHECK` + path + size + mtime; server compares against destination
6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks 6. **Bandwidth limiting** — token-bucket algorithm with `nanosleep` throttling on 64 KB write chunks
7. **Metadata restoration**`chmod()`, `chown()`, `utimensat()` on the receiving side 7. **Metadata restoration**`chmod()`, `chown()`, `utimensat()` on the receiving side
8. **`--delete`** — sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories 8. **`--delete`** — sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories
9. **SSH transport**`socketpair()` + `fork()` + `execvp("ssh", ...)` with `ControlMaster` and port support 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()` 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 ## Build Requirements
@@ -294,21 +223,6 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u
# Bandwidth limit to 1 MB/s # Bandwidth limit to 1 MB/s
./build/client --bwlimit 1024 /src user@host:/dst ./build/client --bwlimit 1024 /src user@host:/dst
# With timeouts, quiet mode, and stats
./build/client --timeout 60 --contimeout 15 --quiet --stats /src user@host:/dst
# Backup overwritten files to a directory
./build/client --backup --backup-dir /backups /src user@host:/dst
# Exclude patterns from file, limit depth
./build/client --exclude-from ignore.txt --max-depth 3 /src user@host:/dst
# 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 # All features
./build/client -a --progress --chunk-size 5242880 --exclude "*.log" --delete /src /dst ./build/client -a --progress --chunk-size 5242880 --exclude "*.log" --delete /src /dst
``` ```
@@ -316,9 +230,7 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u
## Testing ## Testing
```bash ```bash
# Unit tests (18 suites — array_list, chunk, compression, config, data, delta, file, glob, # Unit tests (7 suites)
# metadata, property, protocol, queue, robustness, scanner,
# shared_utils, stress, transport_tcp, transport_ssh, transport_tls)
./build/tests ./build/tests
# Integration + benchmark suite # Integration + benchmark suite
@@ -332,15 +244,12 @@ The benchmark prints throughput metrics, best configuration, and speedup vs rsyn
1. Chunk size (~10 MB default) balances memory and transfer efficiency 1. Chunk size (~10 MB default) balances memory and transfer efficiency
2. Compression level trades CPU for bandwidth 2. Compression level trades CPU for bandwidth
3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files 3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files
4. Multithreading scales with core count; `--queue-size` controls pipeline buffering 4. Multithreading scales with core count
5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled) 5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled)
6. SSH socketpair buffer set to 1 MB for improved pipe throughput 6. SSH socketpair buffer set to 1 MB for improved pipe throughput
7. SSH ControlMaster reuses connections across repeated invocations 7. SSH ControlMaster reuses connections across repeated invocations
8. Incremental sync eliminates redundant transfers entirely 8. Incremental sync eliminates redundant transfers entirely
9. Batch incremental reduces round-trips by grouping multiple checks into one message 9. Bandwidth limiting uses token-bucket with nanosleep for accurate throttling
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 ## Benchmark Results
+63 -172
View File
@@ -3,7 +3,6 @@
#include "delta.h" #include "delta.h"
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include "transport_tcp.h"
#include "transport_tls.h" #include "transport_tls.h"
#include "utils.h" #include "utils.h"
#include <errno.h> #include <errno.h>
@@ -32,8 +31,6 @@ static void print_usage(void) {
printf(" --delete Delete files on receiver not in source\n"); printf(" --delete Delete files on receiver not in source\n");
printf(" --exclude <pattern> Exclude files matching pattern\n"); printf(" --exclude <pattern> Exclude files matching pattern\n");
printf(" --include <pattern> Only include files matching pattern\n"); printf(" --include <pattern> Only include files matching pattern\n");
printf(" --exclude-from <file> Read exclude patterns from file\n");
printf(" --include-from <file> Read include patterns from file\n");
printf(" --max-size <n> Skip files larger than n bytes\n"); printf(" --max-size <n> Skip files larger than n bytes\n");
printf(" --min-size <n> Skip files smaller 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(" --incremental Skip files unchanged since last transfer\n");
@@ -58,118 +55,24 @@ static void print_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(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\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(" --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 Keep partial files on interrupted transfer\n");
printf(" --fastsync-server-path <path>\n");
printf(" Path to fastsync-server on remote (default: fastsync-server)\n");
printf(" --help Show this help\n"); printf(" --help Show this help\n");
} }
static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) {
FILE* fp = fopen(filepath, "r");
if (!fp) {
fprintf(stderr, "Error: could not open pattern file '%s': %s\n", filepath, strerror(errno));
return -1;
}
char line[4096];
while (fgets(line, sizeof(line), fp)) {
char* p = line;
while (*p == ' ' || *p == '\t')
p++;
if (*p == '#' || *p == '\n' || *p == '\0')
continue;
size_t len = strlen(p);
while (len > 0 && (p[len - 1] == '\n' || p[len - 1] == '\r'))
p[--len] = '\0';
if (len == 0)
continue;
char** tmp = realloc(*patterns, (*count + 1) * sizeof(char*));
if (!tmp) {
fprintf(stderr, "Error: memory allocation failed for pattern file\n");
fclose(fp);
return -1;
}
*patterns = tmp;
(*patterns)[(*count)++] = str_dup(p);
}
fclose(fp);
return 0;
}
static bool validate_config(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_incremental && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --incremental");
config->use_metadata = true;
}
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_delta && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta");
config->use_metadata = true;
}
if (config->use_tls) {
if (!config->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n");
return false;
}
tls_global_init();
}
return true;
}
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
const char* env_source = getenv("FASTSYNC_SOURCE_DIR"); const char* env_source = getenv("FASTSYNC_SOURCE_DIR");
const char* env_dest = getenv("FASTSYNC_DEST_DIR"); const char* env_dest = getenv("FASTSYNC_DEST_DIR");
const char* env_save = getenv("FASTSYNC_SAVE_TO_DISK"); const char* env_save = getenv("FASTSYNC_SAVE_TO_DISK");
int exit_code = 0; bool save_to_disk = false;
Config* config = NULL; if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) {
bool config_owned_by_pipeline = false; save_to_disk = true;
config = config_create();
if (!config) {
exit_code = 1;
goto cleanup;
} }
if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0))
config->save_to_disk = true; Config* config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL, save_to_disk, false, false,
false, false, 5, false, 0);
int exit_code = 0;
bool config_owned_by_pipeline = false;
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
@@ -297,69 +200,8 @@ int main(int argc, char* argv[]) {
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
free(config->tls_ca); free(config->tls_ca);
config->tls_ca = str_dup(argv[++i]); config->tls_ca = str_dup(argv[++i]);
} else if (strcmp(argv[i], "--timeout") == 0 && i + 1 < argc) {
config->timeout = atoi(argv[++i]);
if (config->timeout <= 0) {
fprintf(stderr, "Error: --timeout must be a positive integer\n");
exit_code = 1;
goto cleanup;
}
} else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) {
config->contimeout = atoi(argv[++i]);
if (config->contimeout <= 0) {
fprintf(stderr, "Error: --contimeout must be a positive integer\n");
exit_code = 1;
goto cleanup;
}
} 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) {
config->backup = true;
} else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) {
config->backup_dir = str_dup(argv[++i]);
} else if (strcmp(argv[i], "--stats") == 0) {
config->stats = true;
} else if (strcmp(argv[i], "--max-depth") == 0 && i + 1 < argc) {
config->max_depth = atoi(argv[++i]);
if (config->max_depth < 0) {
fprintf(stderr, "Error: --max-depth must be a non-negative integer\n");
exit_code = 1;
goto cleanup;
}
} else if (strcmp(argv[i], "--log-file") == 0 && i + 1 < argc) {
FILE* lf = fopen(argv[++i], "a");
if (!lf) {
fprintf(stderr, "Error: could not open log file '%s': %s\n", argv[i], strerror(errno));
exit_code = 1;
goto cleanup;
}
config->log_file = lf;
log_set_file(lf);
} else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) {
config->queue_size = atoi(argv[++i]);
if (config->queue_size <= 0) {
fprintf(stderr, "Error: --queue-size must be a positive integer\n");
exit_code = 1;
goto cleanup;
}
} else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) {
if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) !=
0) {
exit_code = 1;
goto cleanup;
}
} else if (strcmp(argv[i], "--include-from") == 0 && i + 1 < argc) {
if (read_patterns_from_file(argv[++i], &config->include_patterns, &config->include_count) !=
0) {
exit_code = 1;
goto cleanup;
}
} else if (strcmp(argv[i], "--partial") == 0) { } else if (strcmp(argv[i], "--partial") == 0) {
config->partial = true; config->partial = true;
} else if (strcmp(argv[i], "--fastsync-server-path") == 0 && i + 1 < argc) {
free(config->fastsync_server_path);
config->fastsync_server_path = str_dup(argv[++i]);
} 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 (argv[i][0] == '-') { } else if (argv[i][0] == '-') {
@@ -385,6 +227,7 @@ int main(int argc, char* argv[]) {
config->send_directory = str_dup(argv[positional_args[0]]); config->send_directory = str_dup(argv[positional_args[0]]);
config->receive_root_directory = str_dup(argv[positional_args[1]]); config->receive_root_directory = str_dup(argv[positional_args[1]]);
config->save_to_disk = true; config->save_to_disk = true;
config_parse_ssh_dest(config); config_parse_ssh_dest(config);
} else if (positional_count == 1) { } else if (positional_count == 1) {
fprintf(stderr, "Error: missing destination argument\n"); fprintf(stderr, "Error: missing destination argument\n");
@@ -398,12 +241,64 @@ int main(int argc, char* argv[]) {
config->receive_root_directory = str_dup((char*)env_dest); config->receive_root_directory = str_dup((char*)env_dest);
} }
if (!validate_config(config)) { if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n");
print_usage();
exit_code = 1;
goto cleanup;
}
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");
exit_code = 1; exit_code = 1;
goto cleanup; goto cleanup;
} }
tcp_set_timeouts(config->timeout, config->contimeout); if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
exit_code = 1;
goto cleanup;
}
if (config->use_incremental && config->use_chunk_serialization) {
fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n");
exit_code = 1;
goto cleanup;
}
if (config->use_incremental && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --incremental");
config->use_metadata = true;
}
if (config->use_delta && !config->use_incremental) {
fprintf(stderr, "Error: --delta requires --incremental\n");
exit_code = 1;
goto cleanup;
}
if (config->use_delta && config->use_chunk_serialization) {
fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n");
exit_code = 1;
goto cleanup;
}
if (config->use_delta && config->use_sendfile) {
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
exit_code = 1;
goto cleanup;
}
if (config->use_delta && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta");
config->use_metadata = true;
}
if (config->use_tls) {
if (!config->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n");
exit_code = 1;
goto cleanup;
}
tls_global_init();
}
if (config->use_multithreading) { if (config->use_multithreading) {
config_owned_by_pipeline = true; config_owned_by_pipeline = true;
@@ -413,11 +308,7 @@ int main(int argc, char* argv[]) {
} }
cleanup: cleanup:
if (config) {
if (config->log_file)
fclose(config->log_file);
if (!config_owned_by_pipeline) if (!config_owned_by_pipeline)
config_delete(config); config_delete(config);
}
return exit_code; return exit_code;
} }
+86 -101
View File
@@ -21,9 +21,6 @@
#include <string.h> #include <string.h>
#include <threads.h> #include <threads.h>
#include <time.h> #include <time.h>
#include <unistd.h>
#define STREAM_THRESHOLD (64ULL * 1024 * 1024)
static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) { static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) {
*out_sig = NULL; *out_sig = NULL;
@@ -211,13 +208,10 @@ int send_chunk(Client* client, Chunk* chunk, Config* config) {
return 0; return 0;
} }
bool use_sendfile = config->use_sendfile && !config->use_compression;
for (int i = 0; i < chunk->element_count; i++) { for (int i = 0; i < chunk->element_count; i++) {
File* f = chunk->items[i]; int rc =
if (f == NULL) send_single_file(client, chunk->items[i], config, config->use_incremental, use_sendfile);
continue;
bool stream = f->data->data == NULL && f->data->size > 0;
bool use_sendfile = (config->use_sendfile && !config->use_compression) || stream;
int rc = send_single_file(client, f, config, config->use_incremental, use_sendfile);
if (rc == 1) if (rc == 1)
continue; continue;
if (rc < 0) if (rc < 0)
@@ -234,8 +228,7 @@ static int send_chunks_multithreaded(void* pipeline_context) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return 1; return 1;
} }
client = client_connect_ssh(context->config->ssh_destination, context->config->ssh_port, client = client_connect_ssh(context->config->ssh_destination, context->config->ssh_port);
context->config->fastsync_server_path);
} else if (context->config->use_tls) { } else if (context->config->use_tls) {
client = client_create(); client = client_create();
if (!client || !client_connect_tls(client, context->config->server_host, if (!client || !client_connect_tls(client, context->config->server_host,
@@ -302,14 +295,16 @@ 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( mtx_lock(&context->mutex_scanner);
DirectoryScanner* scanner = directory_scanner_create(
context->config->send_directory, context->config->use_metadata, context->config->chunk_size, context->config->send_directory, context->config->use_metadata, context->config->chunk_size,
context->config->exclude_patterns, context->config->exclude_count, context->config->exclude_patterns, context->config->exclude_count,
context->config->include_patterns, context->config->include_count, context->config->max_size, context->config->include_patterns, context->config->include_count, context->config->max_size,
context->config->min_size, context->config->max_depth, 4); context->config->min_size);
mtx_unlock(&context->mutex_scanner);
Chunk* current_chunk; Chunk* current_chunk;
while ((current_chunk = parallel_scanner_next(scanner)) != NULL) { while ((current_chunk = directory_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++) { for (int i = 0; i < current_chunk->element_count; i++) {
@@ -329,7 +324,7 @@ static int scan_directory_multithreaded(void* pipeline_context) {
cnd_signal(&context->condition_not_empty_scanner); cnd_signal(&context->condition_not_empty_scanner);
mtx_unlock(&context->mutex_scanner); mtx_unlock(&context->mutex_scanner);
parallel_scanner_destroy(scanner); directory_scanner_destroy(scanner);
return thrd_success; return thrd_success;
} }
@@ -348,12 +343,9 @@ static int load_files_multithreaded(void* pipeline_context) {
} }
if (!context->config->use_sendfile) { if (!context->config->use_sendfile) {
for (int i = 0; i < chunk->element_count; i++) { for (int i = 0; i < chunk->element_count; i++) {
File* f = chunk->items[i]; if (!file_load_data(chunk->items[i])) {
if (f->data->size > STREAM_THRESHOLD)
continue;
if (!file_load_data(f)) {
log_message(LOG_LEVEL_ERROR, "Failed to load file data, skipping"); log_message(LOG_LEVEL_ERROR, "Failed to load file data, skipping");
file_destroy(f); file_destroy(chunk->items[i]);
chunk->items[i] = NULL; chunk->items[i] = NULL;
} }
} }
@@ -364,13 +356,12 @@ static int load_files_multithreaded(void* pipeline_context) {
} }
} }
static int run_dry_run(Config* config) { int send_files(Config* config) {
if (config->dry_run) {
DirectoryScanner* scanner = directory_scanner_create( DirectoryScanner* scanner = directory_scanner_create(
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns,
config->exclude_count, config->include_patterns, config->include_count, config->max_size, config->exclude_count, config->include_patterns, config->include_count, config->max_size,
config->min_size, config->max_depth); config->min_size);
if (!scanner)
return -1;
Chunk* chunk; Chunk* chunk;
int file_count = 0; int file_count = 0;
unsigned long long total_bytes = 0; unsigned long long total_bytes = 0;
@@ -388,66 +379,42 @@ static int run_dry_run(Config* config) {
return 0; return 0;
} }
static Client* connect_to_server(Config* config) { Client* client;
if (config->transport == TRANSPORT_SSH) { if (config->transport == TRANSPORT_SSH) {
if (config->use_sendfile) { if (config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return NULL; return 1;
} }
return client_connect_ssh(config->ssh_destination, config->ssh_port, client = client_connect_ssh(config->ssh_destination, config->ssh_port);
config->fastsync_server_path);
}
Client* client = client_create();
if (!client)
return NULL;
bool ok;
if (config->use_tls) {
ok = client_connect_tls(client, config->server_host, config->server_port, config->tls_cert,
config->tls_key, config->tls_ca);
} else {
ok = client_connect(client, config->server_host, config->server_port);
}
if (!ok) {
client_delete(client);
fprintf(stderr, "Error: could not connect to server\n");
return NULL;
}
return client;
}
static bool send_manifest(int fd, ArrayList* manifest) {
if (!send_status(fd, STATUS_MANIFEST))
return false;
if (!send_int(fd, manifest->size))
return false;
for (int i = 0; i < manifest->size; i++) {
if (!send_str(fd, (char*)manifest->items[i]))
return false;
}
return true;
}
static void print_progress(unsigned long long total_bytes, time_t start) {
double elapsed = difftime(time(NULL), start);
double rate = elapsed > 0 ? total_bytes / (1048576.0 * elapsed) : 0;
fprintf(stderr, "\rSent %.1f MB (%.1f MB/s) ", total_bytes / 1048576.0, rate);
fflush(stderr);
}
int send_files(Config* config) {
if (config->dry_run)
return run_dry_run(config);
Client* client = connect_to_server(config);
if (!client) if (!client)
return 1; return 1;
if (!config_send(client->file_descriptor, config)) } else if (config->use_tls) {
goto send_fail; 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_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_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;
}
DirectoryScanner* scanner = directory_scanner_create( DirectoryScanner* scanner = directory_scanner_create(
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns, config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns,
config->exclude_count, config->include_patterns, config->include_count, config->max_size, config->exclude_count, config->include_patterns, config->include_count, config->max_size,
config->min_size, config->max_depth); config->min_size);
Chunk* current_chunk; Chunk* current_chunk;
unsigned long long total_bytes = 0; unsigned long long total_bytes = 0;
time_t last_progress = 0; time_t last_progress = 0;
@@ -466,10 +433,7 @@ int send_files(Config* config) {
} }
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++) {
File* f = current_chunk->items[i]; if (!file_load_data(current_chunk->items[i])) {
if (f->data->size > STREAM_THRESHOLD)
continue;
if (!file_load_data(f)) {
log_message(LOG_LEVEL_ERROR, "Failed to load file data"); log_message(LOG_LEVEL_ERROR, "Failed to load file data");
continue; continue;
} }
@@ -485,26 +449,40 @@ int send_files(Config* config) {
time_t now = time(NULL); time_t now = time(NULL);
if (now - last_progress >= 1) { if (now - last_progress >= 1) {
last_progress = now; last_progress = now;
print_progress(total_bytes, start); double elapsed = difftime(now, start);
double rate = elapsed > 0 ? total_bytes / (1048576.0 * elapsed) : 0;
fprintf(stderr, "\rSent %.1f MB (%.1f MB/s) ", total_bytes / 1048576.0, rate);
fflush(stderr);
} }
} }
chunk_destroy(current_chunk); chunk_destroy(current_chunk);
} }
if (config->use_delete) { if (config->use_delete) {
if (!send_manifest(client->file_descriptor, manifest)) { if (!send_status(client->file_descriptor, STATUS_MANIFEST)) {
array_list_delete(manifest); array_list_delete(manifest);
goto send_fail; goto send_fail;
} }
if (!send_int(client->file_descriptor, manifest->size)) {
array_list_delete(manifest);
goto send_fail;
}
for (int i = 0; i < manifest->size; i++) {
if (!send_str(client->file_descriptor, (char*)manifest->items[i])) {
array_list_delete(manifest);
goto send_fail;
}
}
array_list_delete(manifest); array_list_delete(manifest);
} }
if (!send_status(client->file_descriptor, STATUS_FINISHED)) if (!send_status(client->file_descriptor, STATUS_FINISHED))
goto send_fail; goto send_fail;
Status s; Status s;
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK; int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK;
if (config->show_progress) if (config->show_progress) {
print_progress(total_bytes, start); double elapsed = difftime(time(NULL), start);
if (config->show_progress) double rate = elapsed > 0 ? total_bytes / (1048576.0 * elapsed) : 0;
fprintf(stderr, "Done.\n"); fprintf(stderr, "\rSent %.1f MB (%.1f MB/s) Done.\n", total_bytes / 1048576.0, rate);
}
directory_scanner_destroy(scanner); directory_scanner_destroy(scanner);
client_disconnect(client); client_disconnect(client);
client_delete(client); client_delete(client);
@@ -518,23 +496,30 @@ send_fail:
} }
int send_files_multithreaded(Config* config) { int send_files_multithreaded(Config* config) {
if (config->dry_run) if (config->dry_run) {
return run_dry_run(config); DirectoryScanner* scanner = directory_scanner_create(
config->send_directory, config->use_metadata, config->chunk_size, config->exclude_patterns,
config->exclude_count, config->include_patterns, config->include_count, config->max_size,
config->min_size);
Chunk* chunk;
int file_count = 0;
unsigned long long total_bytes = 0;
printf("Dry run: files to be transferred\n");
while ((chunk = directory_scanner_next(scanner)) != NULL) {
for (int i = 0; i < chunk->element_count; i++) {
printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size);
total_bytes += chunk->items[i]->data->size;
file_count++;
}
chunk_destroy(chunk);
}
directory_scanner_destroy(scanner);
printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0);
return 0;
}
long pages = sysconf(_SC_AVPHYS_PAGES); Queue* q1 = queue_create(100, chunk_destroy);
long page_size = sysconf(_SC_PAGE_SIZE); Queue* q2 = queue_create(100, chunk_destroy);
unsigned long long available_memory =
pages > 0 && page_size > 0 ? (unsigned long long)pages * (unsigned long long)page_size
: 512ULL * 1024 * 1024;
unsigned long long avg_file_size = 1024 * 1024;
int qsize = (int)(available_memory / avg_file_size);
if (qsize < 10)
qsize = 10;
if (qsize > 1000)
qsize = 1000;
Queue* q1 = queue_create(qsize, chunk_destroy);
Queue* q2 = queue_create(qsize, chunk_destroy);
if (!q1 || !q2) { if (!q1 || !q2) {
if (q1) if (q1)
queue_destroy(q1); queue_destroy(q1);
+139 -299
View File
@@ -9,53 +9,89 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <threads.h>
#include <unistd.h> #include <unistd.h>
typedef struct { DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata,
char* path;
int depth;
} DirEntry;
static void dir_entry_destroy(void* item) {
if (item) {
DirEntry* de = (DirEntry*)item;
free(de->path);
free(de);
}
}
static DirEntry* dir_entry_create(const char* path, int depth) {
DirEntry* de = malloc(sizeof(DirEntry));
if (de) {
de->path = str_dup(path);
de->depth = depth;
}
return de;
}
DirectoryScanner* directory_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) {
return directory_scanner_create_full(root_directory, use_metadata, chunk_size,
exclude_patterns, exclude_count,
include_patterns, include_count,
max_size, min_size, true);
}
DirectoryScanner* directory_scanner_create_full(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, bool follow_symlinks) {
DirectoryScanner* scanner = malloc(sizeof(DirectoryScanner)); DirectoryScanner* scanner = malloc(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, free);
scanner->current_dir = NULL; scanner->current_dir = NULL;
scanner->current_path = NULL; scanner->current_path = NULL;
scanner->use_metadata = use_metadata; scanner->use_metadata = use_metadata;
scanner->chunk_size = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE; scanner->chunk_size = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE;
scanner->exclude_patterns = exclude_patterns; /* Deep-copy exclude patterns */
if (exclude_count > 0 && exclude_patterns != NULL) {
scanner->exclude_patterns = malloc((size_t)exclude_count * sizeof(char*));
if (scanner->exclude_patterns == NULL) {
queue_destroy(scanner->directories);
free(scanner);
return NULL;
}
for (int i = 0; i < exclude_count; i++) {
scanner->exclude_patterns[i] = str_dup(exclude_patterns[i]);
if (scanner->exclude_patterns[i] == NULL) {
for (int j = 0; j < i; j++)
free(scanner->exclude_patterns[j]);
free(scanner->exclude_patterns);
queue_destroy(scanner->directories);
free(scanner);
return NULL;
}
}
} else {
scanner->exclude_patterns = NULL;
}
scanner->exclude_count = exclude_count; scanner->exclude_count = exclude_count;
scanner->include_patterns = include_patterns;
/* Deep-copy include patterns */
if (include_count > 0 && include_patterns != NULL) {
scanner->include_patterns = malloc((size_t)include_count * sizeof(char*));
if (scanner->include_patterns == NULL) {
for (int i = 0; i < exclude_count; i++)
free(scanner->exclude_patterns[i]);
free(scanner->exclude_patterns);
queue_destroy(scanner->directories);
free(scanner);
return NULL;
}
for (int i = 0; i < include_count; i++) {
scanner->include_patterns[i] = str_dup(include_patterns[i]);
if (scanner->include_patterns[i] == NULL) {
for (int j = 0; j < i; j++)
free(scanner->include_patterns[j]);
free(scanner->include_patterns);
for (int j = 0; j < exclude_count; j++)
free(scanner->exclude_patterns[j]);
free(scanner->exclude_patterns);
queue_destroy(scanner->directories);
free(scanner);
return NULL;
}
}
} else {
scanner->include_patterns = NULL;
}
scanner->include_count = include_count; scanner->include_count = include_count;
scanner->max_size = max_size; scanner->max_size = max_size;
scanner->min_size = min_size; scanner->min_size = min_size;
scanner->max_depth = max_depth; scanner->follow_symlinks = follow_symlinks;
scanner->current_depth = 0; queue_enqueue(scanner->directories, str_dup(root_directory));
queue_enqueue(scanner->directories, dir_entry_create(root_directory, 0));
return scanner; return scanner;
} }
@@ -67,6 +103,12 @@ void directory_scanner_destroy(DirectoryScanner* scanner) {
scanner->current_dir = NULL; scanner->current_dir = NULL;
} }
free(scanner->current_path); free(scanner->current_path);
for (int i = 0; i < scanner->exclude_count; i++)
free(scanner->exclude_patterns[i]);
free(scanner->exclude_patterns);
for (int i = 0; i < scanner->include_count; i++)
free(scanner->include_patterns[i]);
free(scanner->include_patterns);
queue_destroy(scanner->directories); queue_destroy(scanner->directories);
free(scanner); free(scanner);
} }
@@ -80,6 +122,7 @@ static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
return chunk; return chunk;
} }
// Returns: 1 on success, 0 if no more directories in queue, -1 on opendir failure
static int open_next_directory(DirectoryScanner* scanner) { static int open_next_directory(DirectoryScanner* scanner) {
if (scanner->current_dir) { if (scanner->current_dir) {
closedir(scanner->current_dir); closedir(scanner->current_dir);
@@ -90,10 +133,7 @@ static int open_next_directory(DirectoryScanner* scanner) {
if (queue_is_empty(scanner->directories)) if (queue_is_empty(scanner->directories))
return 0; return 0;
DirEntry* de = (DirEntry*)queue_dequeue(scanner->directories); scanner->current_path = (char*)queue_dequeue(scanner->directories);
scanner->current_path = de->path;
scanner->current_depth = de->depth;
free(de);
scanner->current_dir = opendir(scanner->current_path); scanner->current_dir = opendir(scanner->current_path);
if (scanner->current_dir == NULL) { if (scanner->current_dir == NULL) {
perror("Could not open directory"); perror("Could not open directory");
@@ -131,24 +171,77 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
char* cur_path = path_cat(scanner->current_path, entry->d_name); char* cur_path = path_cat(scanner->current_path, entry->d_name);
struct stat stats; struct stat stats;
if (stat(cur_path, &stats) != 0) { // Use lstat to detect symlinks
if (lstat(cur_path, &stats) != 0) {
free(cur_path); free(cur_path);
continue; continue;
} }
if (S_ISDIR(stats.st_mode)) { // If follow_symlinks is enabled and this is a symlink, resolve it
int next_depth = scanner->current_depth + 1; if (scanner->follow_symlinks && S_ISLNK(stats.st_mode)) {
if (scanner->max_depth <= 0 || next_depth < scanner->max_depth) { struct stat target_stats;
DirEntry* de = dir_entry_create(cur_path, next_depth); if (stat(cur_path, &target_stats) != 0) {
if (!queue_enqueue(scanner->directories, de)) // Broken symlink, skip
dir_entry_destroy(de);
}
free(cur_path);
} else {
if (scanner->max_depth > 0 && scanner->current_depth + 1 > scanner->max_depth) {
free(cur_path); free(cur_path);
continue; continue;
} }
stats = target_stats;
}
if (S_ISDIR(stats.st_mode)) {
queue_enqueue(scanner->directories, (void*)cur_path);
} else if (S_ISLNK(stats.st_mode)) {
// Handle symlink (not following)
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;
}
}
File* file = file_create(cur_path);
if (file == NULL) {
free(cur_path);
continue;
}
file->type = FILE_TYPE_SYMLINK;
// Read link target
char link_buf[4096];
ssize_t link_len = readlink(cur_path, link_buf, sizeof(link_buf) - 1);
if (link_len >= 0) {
link_buf[link_len] = '\0';
file->link_target = str_dup(link_buf);
}
file->data->size = 0;
if (scanner->use_metadata)
file->metadata = file_metadata_create(&stats);
array_list_add(chunk_data, file);
chunk_data_size += 1; // small size for symlinks
if (chunk_data_size > scanner->chunk_size) {
free(cur_path);
return chunk_data_to_chunk(chunk_data);
}
free(cur_path);
} else {
bool excluded = false; bool excluded = false;
for (int i = 0; i < scanner->exclude_count; i++) { for (int i = 0; i < scanner->exclude_count; i++) {
if (glob_match(scanner->exclude_patterns[i], entry->d_name)) { if (glob_match(scanner->exclude_patterns[i], entry->d_name)) {
@@ -204,256 +297,3 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
array_list_delete(chunk_data); array_list_delete(chunk_data);
return NULL; return NULL;
} }
typedef struct {
ParallelScanner* ps;
char** dirs;
int dir_count;
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;
} ParallelWorkerArg;
static int parallel_worker_thread(void* arg) {
ParallelWorkerArg* wa = (ParallelWorkerArg*)arg;
for (int i = 0; i < wa->dir_count; i++) {
DirectoryScanner* ds = directory_scanner_create(
wa->dirs[i], wa->use_metadata, wa->chunk_size, wa->exclude_patterns, wa->exclude_count,
wa->include_patterns, wa->include_count, wa->max_size, wa->min_size, wa->max_depth);
Chunk* chunk;
while ((chunk = directory_scanner_next(ds)) != NULL) {
queue_enqueue_multithreaded(wa->ps->result_queue, chunk, &wa->ps->result_mutex,
&wa->ps->result_not_empty, &wa->ps->result_not_full);
}
directory_scanner_destroy(ds);
free(wa->dirs[i]);
}
ParallelScanner* ps = wa->ps;
free(wa->dirs);
free(wa);
mtx_lock(&ps->result_mutex);
ps->completed++;
if (ps->completed >= ps->num_threads) {
ps->done = true;
cnd_signal(&ps->result_not_empty);
}
mtx_unlock(&ps->result_mutex);
return thrd_success;
}
ParallelScanner* parallel_scanner_create(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) {
ParallelScanner* ps = calloc(1, sizeof(ParallelScanner));
if (!ps)
return NULL;
ps->result_queue = queue_create(100, chunk_destroy);
if (!ps->result_queue) {
free(ps);
return NULL;
}
if (mtx_init(&ps->result_mutex, mtx_plain) != thrd_success ||
cnd_init(&ps->result_not_empty) != thrd_success ||
cnd_init(&ps->result_not_full) != thrd_success) {
queue_destroy(ps->result_queue);
free(ps);
return NULL;
}
DIR* dir = opendir(root_directory);
if (!dir) {
perror("Could not open root directory for parallel scan");
parallel_scanner_destroy(ps);
return NULL;
}
ArrayList* root_files = array_list_create(file_destroy);
ArrayList* subdirs = array_list_create(free);
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
continue;
char* cur_path = path_cat(root_directory, entry->d_name);
if (!cur_path)
continue;
struct stat st;
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);
free(cur_path);
if (!file)
continue;
file->data->size = st.st_size;
if (use_metadata)
file->metadata = file_metadata_create(&st);
array_list_add(root_files, file);
}
}
closedir(dir);
unsigned long long cs = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE;
if (root_files->size > 0) {
ArrayList* batch = array_list_create(NULL);
unsigned long long batch_size = 0;
Chunk* first = NULL;
for (int i = 0; i < root_files->size; i++) {
File* f = (File*)root_files->items[i];
array_list_add(batch, f);
batch_size += f->data->size;
if (batch_size >= cs || i == root_files->size - 1) {
void** items = array_list_to_array(batch);
Chunk* c = chunk_create((File**)items, batch->size);
free(items);
batch->item_destroyer = NULL;
array_list_delete(batch);
batch = NULL;
if (!first) {
first = c;
} else {
queue_enqueue_multithreaded(ps->result_queue, c, &ps->result_mutex, &ps->result_not_empty,
&ps->result_not_full);
}
if (i < root_files->size - 1) {
batch = array_list_create(NULL);
batch_size = 0;
}
}
}
if (batch) {
batch->item_destroyer = NULL;
array_list_delete(batch);
}
ps->initial_chunk = first;
root_files->item_destroyer = NULL;
}
array_list_delete(root_files);
int n = num_threads > 0 ? num_threads : 4;
if (n > subdirs->size)
n = subdirs->size > 0 ? subdirs->size : 1;
if (subdirs->size > 0) {
ps->num_threads = n;
ps->threads = calloc(n, sizeof(thrd_t));
if (!ps->threads) {
array_list_delete(subdirs);
parallel_scanner_destroy(ps);
return NULL;
}
int dirs_per_thread = subdirs->size / n;
int remainder = subdirs->size % n;
int start = 0;
for (int t = 0; t < n; t++) {
int count = dirs_per_thread + (t < remainder ? 1 : 0);
if (count == 0)
break;
ParallelWorkerArg* wa = calloc(1, sizeof(ParallelWorkerArg));
if (!wa)
break;
wa->ps = ps;
wa->dirs = calloc(count, sizeof(char*));
if (!wa->dirs) {
free(wa);
break;
}
for (int j = 0; j < count; j++)
wa->dirs[j] = str_dup((char*)subdirs->items[start + j]);
wa->dir_count = count;
wa->use_metadata = use_metadata;
wa->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;
start += count;
if (thrd_create(&ps->threads[t], parallel_worker_thread, wa) != thrd_success) {
for (int j = 0; j < count; j++)
free(wa->dirs[j]);
free(wa->dirs);
free(wa);
ps->num_threads = t;
break;
}
}
}
array_list_delete(subdirs);
return ps;
}
Chunk* parallel_scanner_next(ParallelScanner* ps) {
if (ps->initial_chunk) {
Chunk* c = ps->initial_chunk;
ps->initial_chunk = NULL;
return c;
}
if (ps->num_threads == 0) {
ps->done = true;
return NULL;
}
Chunk* chunk = queue_dequeue_multithreaded(
ps->result_queue, &ps->result_mutex, &ps->result_not_empty, &ps->result_not_full, &ps->done);
return chunk;
}
void parallel_scanner_destroy(ParallelScanner* ps) {
if (!ps)
return;
ps->done = true;
cnd_signal(&ps->result_not_empty);
for (int i = 0; i < ps->num_threads; i++)
thrd_join(ps->threads[i], NULL);
free(ps->threads);
if (ps->initial_chunk)
chunk_destroy(ps->initial_chunk);
queue_destroy(ps->result_queue);
mtx_destroy(&ps->result_mutex);
cnd_destroy(&ps->result_not_empty);
cnd_destroy(&ps->result_not_full);
free(ps);
}
+8 -26
View File
@@ -5,7 +5,6 @@
#include "queue.h" #include "queue.h"
#include <dirent.h> #include <dirent.h>
#include <stdbool.h> #include <stdbool.h>
#include <threads.h>
typedef struct { typedef struct {
Queue* directories; Queue* directories;
@@ -19,37 +18,20 @@ typedef struct {
int include_count; int include_count;
unsigned long long max_size; unsigned long long max_size;
unsigned long long min_size; unsigned long long min_size;
int max_depth; bool follow_symlinks;
int current_depth;
} DirectoryScanner; } DirectoryScanner;
typedef struct { DirectoryScanner* directory_scanner_create(char* root_directory, bool use_metadata,
Queue* result_queue;
mtx_t result_mutex;
cnd_t result_not_empty;
cnd_t result_not_full;
int num_threads;
thrd_t* threads;
bool done;
int completed;
Chunk* initial_chunk;
} ParallelScanner;
DirectoryScanner* directory_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);
DirectoryScanner* directory_scanner_create_full(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, bool follow_symlinks);
Chunk* directory_scanner_next(DirectoryScanner* scanner); Chunk* directory_scanner_next(DirectoryScanner* scanner);
void directory_scanner_destroy(DirectoryScanner* scanner); void directory_scanner_destroy(DirectoryScanner* scanner);
ParallelScanner* parallel_scanner_create(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);
Chunk* parallel_scanner_next(ParallelScanner* scanner);
void parallel_scanner_destroy(ParallelScanner* scanner);
#endif #endif
+43 -44
View File
@@ -11,26 +11,53 @@
#include "transport_tls.h" #include "transport_tls.h"
#include "unistd.h" #include "unistd.h"
#include "utils.h" #include "utils.h"
#include <libgen.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>
// Check if a file path should be excluded based on config patterns
static bool is_excluded(const char* path, const Config* config) {
// Extract filename from path
char* path_dup = str_dup(path);
if (!path_dup)
return false;
char* fname = basename(path_dup);
// Check exclude patterns
for (int i = 0; i < config->exclude_count; i++) {
if (glob_match(config->exclude_patterns[i], fname)) {
free(path_dup);
return true;
}
}
// Check include patterns (if any, file must match at least one)
if (config->include_count > 0) {
bool included = false;
for (int i = 0; i < config->include_count; i++) {
if (glob_match(config->include_patterns[i], fname)) {
included = true;
break;
}
}
if (!included) {
free(path_dup);
return true;
}
}
free(path_dup);
return false;
}
int receive_files(Config* config, int fd) { int receive_files(Config* config, int fd) {
Status status; Status status;
if (!receive_status(fd, &status)) if (!receive_status(fd, &status))
return -1; return -1;
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) {
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
if (status == STATUS_KEEPALIVE) {
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) { if (status == STATUS_CHECK) {
bool skipped; bool skipped;
File* file = receive_incremental_check(fd, config, &skipped); File* file = receive_incremental_check(fd, config, &skipped);
@@ -38,8 +65,8 @@ int receive_files(Config* config, int fd) {
goto next; goto next;
if (file == NULL && !skipped) if (file == NULL && !skipped)
return -1; return -1;
if (config->save_to_disk) if (config->save_to_disk && !is_excluded(file->path, config))
file_save_to_disk(config->receive_root_directory, file, NULL); file_save_to_disk(config->receive_root_directory, file);
file_destroy(file); file_destroy(file);
} else if (status == STATUS_CHUNK) { } else if (status == STATUS_CHUNK) {
Chunk* chunk = receive_chunk_data(fd, config); Chunk* chunk = receive_chunk_data(fd, config);
@@ -48,38 +75,10 @@ int receive_files(Config* config, int fd) {
return -1; return -1;
} }
for (int i = 0; i < chunk->element_count; i++) { for (int i = 0; i < chunk->element_count; i++) {
if (config->save_to_disk) if (config->save_to_disk && !is_excluded(chunk->items[i]->path, config))
file_save_to_disk(config->receive_root_directory, chunk->items[i], NULL); file_save_to_disk(config->receive_root_directory, chunk->items[i]);
} }
chunk_destroy(chunk); 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 && stat(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 { } else {
File* file = file_receive(config, fd); File* file = file_receive(config, fd);
if (file == NULL) { if (file == NULL) {
@@ -87,8 +86,8 @@ int receive_files(Config* config, int fd) {
send_status(fd, STATUS_ERROR); send_status(fd, STATUS_ERROR);
return -1; return -1;
} }
if (config->save_to_disk) if (config->save_to_disk && !is_excluded(file->path, config))
file_save_to_disk(config->receive_root_directory, file, NULL); file_save_to_disk(config->receive_root_directory, file);
file_destroy(file); file_destroy(file);
} }
next: next:
+12 -21
View File
@@ -1,29 +1,12 @@
#include "compression.h" #include "compression.h"
#include "data.h" #include "data.h"
#include "log.h" #include "log.h"
#include "stdlib.h" #include <stdint.h>
#include "string.h" #include <stdlib.h>
#include <strings.h>
#include "zstd.h" #include "zstd.h"
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024) #define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
static const char* SKIP_COMPRESSION_EXTENSIONS[] = {".jpg", ".jpeg", ".png", ".gif", ".mp4", ".mkv",
".zip", ".gz", ".xz", ".zst", NULL};
bool compression_should_skip(const char* path) {
if (!path)
return false;
const char* dot = strrchr(path, '.');
if (!dot)
return false;
for (int i = 0; SKIP_COMPRESSION_EXTENSIONS[i]; i++) {
if (strcasecmp(dot, SKIP_COMPRESSION_EXTENSIONS[i]) == 0)
return true;
}
return false;
}
Data* data_compress(Data* data_to_compress, int compression_level) { Data* data_compress(Data* data_to_compress, int compression_level) {
log_message(LOG_LEVEL_DEBUG, "Starting to compress data"); log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
size_t dst_size = ZSTD_compressBound(data_to_compress->size); size_t dst_size = ZSTD_compressBound(data_to_compress->size);
@@ -84,8 +67,16 @@ Data* data_decompress(Data* compressed_data) {
return NULL; return NULL;
} }
size_t buf_size = size_t buf_size = INITIAL_DECOMPRESS_BUF_SIZE;
(!ZSTD_isError(dst_size) && dst_size > 0) ? (size_t)dst_size : INITIAL_DECOMPRESS_BUF_SIZE; if (!ZSTD_isError(dst_size) && dst_size > 0) {
if (dst_size > SIZE_MAX) {
log_message(LOG_LEVEL_ERROR,
"Decompressed size %llu exceeds addressable memory, using fallback buffer",
dst_size);
} else {
buf_size = (size_t)dst_size;
}
}
Data* uncompressed_data = data_create_empty(buf_size); Data* uncompressed_data = data_create_empty(buf_size);
if (!uncompressed_data) { if (!uncompressed_data) {
log_message(LOG_LEVEL_ERROR, "Failed to allocate decompression buffer"); log_message(LOG_LEVEL_ERROR, "Failed to allocate decompression buffer");
-2
View File
@@ -2,10 +2,8 @@
#define COMPRESSION_H #define COMPRESSION_H
#include "data.h" #include "data.h"
#include <stdbool.h>
Data* data_compress(Data* data_to_compress, int compression_level); Data* data_compress(Data* data_to_compress, int compression_level);
Data* data_decompress(Data* compressed_data); Data* data_decompress(Data* compressed_data);
bool compression_should_skip(const char* path);
#endif #endif
+118 -29
View File
@@ -8,21 +8,47 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
Config* config_create(void) { Config* config_create(char* version, char* send_directory, char* receive_directory,
Config* config = calloc(1, sizeof(Config)); bool save_to_disk, bool use_multithreading, bool use_chunk_serialization,
if (!config) bool use_compression, bool use_metadata, int compression_level,
return NULL; bool use_sendfile, unsigned long long chunk_size) {
config->version = str_dup(PROTOCOL_VERSION);
config->compression_level = 5; Config* config = malloc(sizeof(Config));
config->chunk_size = DEFAULT_CHUNK_SIZE; config->version = version;
config->send_directory = send_directory;
config->receive_root_directory = receive_directory;
config->save_to_disk = save_to_disk;
config->use_multithreading = use_multithreading;
config->use_chunk_serialization = use_chunk_serialization;
config->use_compression = use_compression;
config->use_metadata = use_metadata;
config->show_progress = false;
config->dry_run = false;
config->use_delete = false;
config->compression_level = compression_level;
config->use_sendfile = use_sendfile;
config->chunk_size = chunk_size > 0 ? chunk_size : DEFAULT_CHUNK_SIZE;
config->ssh_port = 22; config->ssh_port = 22;
config->server_port = 8080; config->transport = TRANSPORT_TCP;
config->server_host = str_dup("127.0.0.1"); config->ssh_destination = NULL;
config->timeout = 30; config->exclude_patterns = NULL;
config->contimeout = 10; config->exclude_count = 0;
config->queue_size = 100; config->include_patterns = NULL;
config->include_count = 0;
config->max_size = 0;
config->min_size = 0;
config->use_incremental = false;
config->use_delta = false;
config->delta_block_size = DELTA_BLOCK_SIZE_DEFAULT; config->delta_block_size = DELTA_BLOCK_SIZE_DEFAULT;
config->delta_max_file_size = DELTA_MAX_FILE_SIZE; config->delta_max_file_size = DELTA_MAX_FILE_SIZE;
config->use_tls = false;
config->tls_cert = NULL;
config->tls_key = NULL;
config->tls_ca = NULL;
config->follow_symlinks = false;
config->partial = false;
config->server_host = str_dup("127.0.0.1");
config->server_port = 8080;
return config; return config;
} }
@@ -57,7 +83,6 @@ void config_delete(Config* config) {
free(config->send_directory); free(config->send_directory);
free(config->receive_root_directory); free(config->receive_root_directory);
free(config->ssh_destination); free(config->ssh_destination);
free(config->fastsync_server_path);
for (int i = 0; i < config->exclude_count; i++) for (int i = 0; i < config->exclude_count; i++)
free(config->exclude_patterns[i]); free(config->exclude_patterns[i]);
free(config->exclude_patterns); free(config->exclude_patterns);
@@ -67,7 +92,6 @@ void config_delete(Config* config) {
free(config->tls_cert); free(config->tls_cert);
free(config->tls_key); free(config->tls_key);
free(config->tls_ca); free(config->tls_ca);
free(config->backup_dir);
free(config->server_host); free(config->server_host);
free(config); free(config);
} }
@@ -105,9 +129,25 @@ bool config_send(int file_descriptor, const Config* config) {
return false; return false;
if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long))) if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long)))
return false; return false;
if (!send_int(file_descriptor, config->backup)) if (!send_int(file_descriptor, config->exclude_count))
return false; return false;
if (!send_str(file_descriptor, config->backup_dir ? config->backup_dir : "")) for (int i = 0; i < config->exclude_count; i++) {
if (!send_str(file_descriptor, config->exclude_patterns[i]))
return false;
}
if (!send_int(file_descriptor, config->include_count))
return false;
for (int i = 0; i < config->include_count; i++) {
if (!send_str(file_descriptor, config->include_patterns[i]))
return false;
}
if (!send_n_data(file_descriptor, &config->max_size, sizeof(config->max_size)))
return false;
if (!send_n_data(file_descriptor, &config->min_size, sizeof(config->min_size)))
return false;
if (!send_int(file_descriptor, config->follow_symlinks))
return false;
if (!send_int(file_descriptor, config->partial))
return false; return false;
Status status; Status status;
if (!receive_status(file_descriptor, &status)) if (!receive_status(file_descriptor, &status))
@@ -193,7 +233,6 @@ Config* config_receive(int file_descriptor) {
config->ssh_port = 22; config->ssh_port = 22;
config->transport = TRANSPORT_TCP; config->transport = TRANSPORT_TCP;
config->ssh_destination = NULL; config->ssh_destination = NULL;
config->fastsync_server_path = NULL;
config->exclude_patterns = NULL; config->exclude_patterns = NULL;
config->exclude_count = 0; config->exclude_count = 0;
config->include_patterns = NULL; config->include_patterns = NULL;
@@ -204,19 +243,70 @@ Config* config_receive(int file_descriptor) {
config->tls_cert = NULL; config->tls_cert = NULL;
config->tls_key = NULL; config->tls_key = NULL;
config->tls_ca = NULL; config->tls_ca = NULL;
config->timeout = 30; config->follow_symlinks = false;
config->contimeout = 10; config->partial = false;
config->quiet = false;
config->stats = false; // Receive exclude patterns
config->max_depth = 0; int ec;
config->log_file = NULL; if (!receive_int(file_descriptor, &ec))
config->queue_size = 100;
if (!receive_int(file_descriptor, &tmp))
goto error; goto error;
config->backup = tmp; config->exclude_count = ec;
config->backup_dir = receive_str(file_descriptor); if (ec > 0) {
if (config->backup_dir == NULL) config->exclude_patterns = malloc((size_t)ec * sizeof(char*));
if (!config->exclude_patterns) {
config->exclude_count = 0;
goto error; goto error;
}
for (int i = 0; i < ec; i++) {
config->exclude_patterns[i] = receive_str(file_descriptor);
if (!config->exclude_patterns[i]) {
for (int j = 0; j < i; j++)
free(config->exclude_patterns[j]);
free(config->exclude_patterns);
config->exclude_patterns = NULL;
config->exclude_count = 0;
goto error;
}
}
}
// Receive include patterns
int ic;
if (!receive_int(file_descriptor, &ic))
goto error;
config->include_count = ic;
if (ic > 0) {
config->include_patterns = malloc((size_t)ic * sizeof(char*));
if (!config->include_patterns) {
config->include_count = 0;
goto error;
}
for (int i = 0; i < ic; i++) {
config->include_patterns[i] = receive_str(file_descriptor);
if (!config->include_patterns[i]) {
for (int j = 0; j < i; j++)
free(config->include_patterns[j]);
free(config->include_patterns);
config->include_patterns = NULL;
config->include_count = 0;
goto error;
}
}
}
if (!receive_n_data(file_descriptor, &config->max_size, sizeof(config->max_size)))
goto error;
if (!receive_n_data(file_descriptor, &config->min_size, sizeof(config->min_size)))
goto error;
int tmp_follow;
if (!receive_int(file_descriptor, &tmp_follow))
goto error;
config->follow_symlinks = tmp_follow;
int tmp_partial;
if (!receive_int(file_descriptor, &tmp_partial))
goto error;
config->partial = tmp_partial;
config->server_host = str_dup("127.0.0.1"); config->server_host = str_dup("127.0.0.1");
config->server_port = 8080; config->server_port = 8080;
if (!send_status(file_descriptor, STATUS_OK)) if (!send_status(file_descriptor, STATUS_OK))
@@ -228,7 +318,6 @@ error:
free(config->send_directory); free(config->send_directory);
free(config->receive_root_directory); free(config->receive_root_directory);
free(config->server_host); free(config->server_host);
free(config->backup_dir);
free(config); free(config);
return NULL; return NULL;
} }
+4 -12
View File
@@ -3,7 +3,6 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h>
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType; typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
@@ -25,7 +24,6 @@ typedef struct Config {
int ssh_port; int ssh_port;
TransportType transport; TransportType transport;
char* ssh_destination; char* ssh_destination;
char* fastsync_server_path;
char** exclude_patterns; char** exclude_patterns;
int exclude_count; int exclude_count;
char** include_patterns; char** include_patterns;
@@ -42,15 +40,6 @@ typedef struct Config {
char* tls_cert; char* tls_cert;
char* tls_key; char* tls_key;
char* tls_ca; char* tls_ca;
int timeout;
int contimeout;
bool quiet;
bool backup;
char* backup_dir;
bool stats;
int max_depth;
FILE* log_file;
int queue_size;
bool follow_symlinks; bool follow_symlinks;
bool partial; bool partial;
} Config; } Config;
@@ -58,7 +47,10 @@ typedef struct Config {
#define PROTOCOL_VERSION "1.3.0" #define PROTOCOL_VERSION "1.3.0"
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
Config* config_create(void); Config* config_create(char* version, char* send_directory, char* receive_directory,
bool save_to_disk, bool use_multithreading, bool use_chunk_serialization,
bool use_compression, bool use_metadata, int compression_level,
bool use_sendfile, unsigned long long chunk_size);
void config_delete(Config* config); void config_delete(Config* config);
bool config_send(int file_descriptor, const Config* config); bool config_send(int file_descriptor, const Config* config);
Config* config_receive(int file_descriptor); Config* config_receive(int file_descriptor);
+1 -1
View File
@@ -1,6 +1,6 @@
#include "data.h" #include "data.h"
#include "log.h" #include "log.h"
#include <stdlib.h> #include "stdlib.h"
Data* data_create_empty(size_t data_size) { Data* data_create_empty(size_t data_size) {
/* malloc(0) is UB; allocate at least 1 byte but preserve requested size */ /* malloc(0) is UB; allocate at least 1 byte but preserve requested size */
+1 -1
View File
@@ -1,7 +1,7 @@
#ifndef DATA_H #ifndef DATA_H
#define DATA_H #define DATA_H
#include <stdlib.h> #include "stdlib.h"
typedef struct { typedef struct {
void* data; void* data;
+1 -6
View File
@@ -108,12 +108,7 @@ DeltaSignature* delta_signature_deserialize(const Data* data) {
return NULL; return NULL;
} }
uint64_t blocks_size = (uint64_t)sig->block_count * sizeof(DeltaBlockSig); sig->blocks = malloc(sig->block_count * sizeof(DeltaBlockSig));
if (blocks_size > SIZE_MAX) {
free(sig);
return NULL;
}
sig->blocks = malloc((size_t)blocks_size);
if (!sig->blocks) { if (!sig->blocks) {
free(sig); free(sig);
return NULL; return NULL;
+161 -52
View File
@@ -1,5 +1,4 @@
#include <dirent.h> #include <dirent.h>
#include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <libgen.h> #include <libgen.h>
#include <stddef.h> #include <stddef.h>
@@ -21,6 +20,9 @@
#include "protocol.h" #include "protocol.h"
#include "utils.h" #include "utils.h"
#define STREAM_THRESHOLD (64ULL * 1024 * 1024) /* 64 MB */
#define STREAM_CHUNK_SIZE (1ULL * 1024 * 1024) /* 1 MB */
File* file_create(const char* path) { File* file_create(const char* path) {
File* file = (File*)malloc(sizeof(File)); File* file = (File*)malloc(sizeof(File));
if (file == NULL) { if (file == NULL) {
@@ -43,7 +45,8 @@ File* file_create(const char* path) {
return NULL; return NULL;
} }
file->metadata = NULL; file->metadata = NULL;
file->skip = false; file->type = FILE_TYPE_REGULAR;
file->link_target = NULL;
return file; return file;
} }
@@ -57,6 +60,8 @@ void file_destroy(void* item) {
file->metadata = NULL; file->metadata = NULL;
free(file->path); free(file->path);
file->path = NULL; file->path = NULL;
free(file->link_target);
file->link_target = NULL;
free(file); free(file);
} }
@@ -85,6 +90,14 @@ void file_metadata_destroy(void* metadata) {
bool file_load_data(File* file) { bool file_load_data(File* file) {
if (file == NULL) if (file == NULL)
return false; return false;
// Symlinks have no data to load
if (file->type == FILE_TYPE_SYMLINK)
return true;
// For streaming files, just record the size, don't load into memory
if (file->data->size > STREAM_THRESHOLD) {
// Don't allocate; streaming will read directly from disk
return true;
}
if (file->data->data == NULL) { if (file->data->data == NULL) {
file->data->data = malloc(file->data->size); file->data->data = malloc(file->data->size);
if (file->data->data == NULL) { if (file->data->data == NULL) {
@@ -100,11 +113,70 @@ bool file_load_data(File* file) {
return true; return true;
} }
// Stream file content in chunks without loading entire file into RAM
static bool file_send_streaming(File* file, int file_descriptor) {
unsigned long long total_size = file->data->size;
// Send total size prefix (same wire format as send_data)
if (!send_n_data(file_descriptor, &total_size, sizeof(total_size)))
return false;
FILE* fp = fopen(file->path, "rb");
if (!fp) {
perror("Could not open file for streaming");
return false;
}
char buf[STREAM_CHUNK_SIZE];
unsigned long long remaining = total_size;
while (remaining > 0) {
size_t to_read = (size_t)((remaining < STREAM_CHUNK_SIZE) ? remaining : STREAM_CHUNK_SIZE);
size_t nread = fread(buf, 1, to_read, fp);
if (nread != to_read) {
if (ferror(fp)) {
perror("Read error during streaming");
}
fclose(fp);
return false;
}
if (!send_n_data(file_descriptor, buf, nread)) {
fclose(fp);
return false;
}
remaining -= (unsigned long long)nread;
}
fclose(fp);
return true;
}
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 (send_path && !send_str(file_descriptor, file->path))
return false;
if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false;
// Send file type indicator so receiver can distinguish regular from symlink
int ft = (int)file->type;
if (!send_int(file_descriptor, ft))
return false;
if (file->type == FILE_TYPE_SYMLINK) {
// Send link target, then zero-length data
if (!send_str(file_descriptor, file->link_target ? file->link_target : ""))
return false;
Data empty = {NULL, 0};
return send_data(file_descriptor, &empty);
}
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)) {
// Streaming mode: for large files without compression, stream from disk
if (file->data->size > STREAM_THRESHOLD && compression_level == 0) {
return file_send_streaming(file, file_descriptor);
}
if (compression_level > 0) {
compressed_data = data_compress(file->data, compression_level); compressed_data = data_compress(file->data, compression_level);
if (compressed_data == NULL) { if (compressed_data == NULL) {
log_message(LOG_LEVEL_ERROR, "Failed to compress file data"); log_message(LOG_LEVEL_ERROR, "Failed to compress file data");
@@ -112,14 +184,6 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
} }
data_to_send = compressed_data; data_to_send = compressed_data;
} }
if (send_path && !send_str(file_descriptor, file->path)) {
data_destroy(compressed_data);
return false;
}
if (use_metadata && !metadata_send(file_descriptor, file->metadata)) {
data_destroy(compressed_data);
return false;
}
if (!send_data(file_descriptor, data_to_send)) { if (!send_data(file_descriptor, data_to_send)) {
data_destroy(compressed_data); data_destroy(compressed_data);
return false; return false;
@@ -128,12 +192,19 @@ 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) { bool file_save_to_disk(const char* root_directory, File* file) {
(void)config; if (file->type == FILE_TYPE_SYMLINK && file->link_target) {
if (has_path_traversal(file->path)) { char* disk_path = path_cat((char*)root_directory, file->path);
log_message(LOG_LEVEL_ERROR, "Path traversal detected in file path: %s", file->path); if (disk_path == NULL)
return false; return false;
unlink(disk_path);
bool ok = (symlink(file->link_target, disk_path) == 0);
if (ok && file->metadata)
file_restore_metadata(disk_path, file->metadata);
free(disk_path);
return ok;
} }
char* disk_path = path_cat((char*)root_directory, file->path); char* disk_path = path_cat((char*)root_directory, file->path);
if (disk_path == NULL) if (disk_path == NULL)
return false; return false;
@@ -332,18 +403,23 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
return NULL; return NULL;
} }
if (has_path_traversal(check_path)) {
log_message(LOG_LEVEL_ERROR, "Path traversal detected: %s", check_path);
free(check_path);
send_status(fd, STATUS_ERROR);
return NULL;
}
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 && stat(full_path, &st) == 0); bool has_old_file = (full_path && stat(full_path, &st) == 0);
unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0; unsigned long long old_size = has_old_file ? (unsigned long long)st.st_size : 0;
// Check for partial file if enabled
if (config->partial && !has_old_file && full_path) {
char* partial_path = malloc(strlen(full_path) + 20);
if (partial_path) {
sprintf(partial_path, "%s.fastsync-partial", full_path);
has_old_file = (stat(partial_path, &st) == 0);
if (has_old_file)
old_size = (unsigned long long)st.st_size;
free(partial_path);
}
}
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; (long long)st.st_mtime == check_mtime;
@@ -399,6 +475,26 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
} }
} }
// Read file type indicator
int file_type;
if (!receive_int(fd, &file_type)) {
file_destroy(file);
send_status(fd, STATUS_ERROR);
return NULL;
}
file->type = (FileType)file_type;
if (file->type == FILE_TYPE_SYMLINK) {
char* link_target = receive_str(fd);
if (link_target) {
file->link_target = link_target;
}
Data* empty_data = receive_data(fd);
if (empty_data)
data_destroy(empty_data);
return file;
}
Data* file_data = receive_data(fd); Data* file_data = receive_data(fd);
if (file_data == NULL) { if (file_data == NULL) {
file_destroy(file); file_destroy(file);
@@ -423,68 +519,51 @@ 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 to_disk(const char* path, const void* data, unsigned long long data_size) {
char* tmp_path = NULL; // dirname() may modify its argument and may return a pointer to static storage.
char* directory = NULL; // We must use a copy of the result to be safe.
char* path_dup = str_dup(path); char* path_dup = str_dup(path);
if (!path_dup) if (!path_dup)
return false; return false;
const char* dir_result = dirname(path_dup); const char* dir_result = dirname(path_dup);
directory = str_dup(dir_result); char* directory = str_dup(dir_result);
free(path_dup); free(path_dup);
if (!directory) if (!directory)
return false; return false;
bool ok = true; bool ok = true;
if (!mkdir_r(directory)) if (!mkdir_r(directory)) {
goto done;
size_t path_len = strlen(path);
tmp_path = malloc(path_len + 5);
if (!tmp_path) {
ok = false; ok = false;
goto done; goto done;
} }
memcpy(tmp_path, path, path_len); FILE* file_pointer = fopen(path, "wb");
memcpy(tmp_path + path_len, ".tmp", 5);
FILE* file_pointer = fopen(tmp_path, "wb");
if (file_pointer == NULL) { if (file_pointer == NULL) {
perror("Could not open temporary file"); perror("Could not open File");
ok = false; ok = false;
goto done; goto done;
} }
if (fwrite(data, 1, data_size, file_pointer) != data_size) { if (fwrite(data, 1, data_size, file_pointer) != data_size) {
perror("Failed to write all data to temporary file"); perror("Failed to write all data to disk");
fclose(file_pointer); fclose(file_pointer);
unlink(tmp_path);
ok = false; ok = false;
goto done; goto done;
} }
fclose(file_pointer); fclose(file_pointer);
if (rename(tmp_path, path) != 0) {
perror("Failed to atomically rename temporary file");
unlink(tmp_path);
ok = false;
goto done;
}
done: done:
free(tmp_path);
free(directory); free(directory);
return ok; 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) {
// Handle symlinks
if (file->type == FILE_TYPE_SYMLINK) {
return file_send_single_calls(file, file_descriptor, use_metadata, compression_level,
send_path);
}
// sendfile is incompatible with compression (kernel zero-copy). // sendfile is incompatible with compression (kernel zero-copy).
// If compression is requested, fall back to the regular send path. // If compression is requested, fall back to the regular send path.
// NOTE: This is a safety net only — callers must ensure compression_level == 0
// before calling file_send_sendfile. The fallback to file_send_single_calls
// preserves the send_path contract, but callers should not rely on it for
// correctness (the --sendfile flag is validated to be mutually exclusive with
// -c/--compress at the CLI layer).
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);
@@ -494,6 +573,11 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
if (use_metadata && !metadata_send(file_descriptor, file->metadata)) if (use_metadata && !metadata_send(file_descriptor, file->metadata))
return false; return false;
// Send file type indicator
int ft = (int)file->type;
if (!send_int(file_descriptor, ft))
return false;
int fd = open(file->path, O_RDONLY); int fd = open(file->path, O_RDONLY);
if (fd == -1) { if (fd == -1) {
perror("Could not open file for sendfile"); perror("Could not open file for sendfile");
@@ -536,6 +620,30 @@ File* file_receive(const Config* config, int file_descriptor) {
return NULL; return NULL;
} }
} }
// Receive file type indicator
int file_type;
if (!receive_int(file_descriptor, &file_type)) {
file_destroy(file);
return NULL;
}
file->type = (FileType)file_type;
if (file->type == FILE_TYPE_SYMLINK) {
char* link_target = receive_str(file_descriptor);
if (link_target == NULL) {
file_destroy(file);
return NULL;
}
file->link_target = link_target;
// Receive and discard zero-length data
Data* empty_data = receive_data(file_descriptor);
if (empty_data)
data_destroy(empty_data);
return file;
}
// Regular file - receive data
Data* file_data = receive_data(file_descriptor); Data* file_data = receive_data(file_descriptor);
if (file_data == NULL) { if (file_data == NULL) {
file_destroy(file); file_destroy(file);
@@ -550,6 +658,7 @@ File* file_receive(const Config* config, int file_descriptor) {
} }
file_data = file_data_uncompressed; file_data = file_data_uncompressed;
} }
data_destroy(file->data); data_destroy(file->data);
file->data = file_data; file->data = file_data;
return file; return file;
+3 -2
View File
@@ -20,7 +20,8 @@ typedef struct {
char* path; char* path;
Data* data; Data* data;
FileMetadata* metadata; FileMetadata* metadata;
bool skip; FileType type;
char* link_target;
} File; } File;
File* file_create(const char* path); File* file_create(const char* path);
@@ -35,7 +36,7 @@ size_t file_content_to_buffer(File* file);
FileMetadata* file_metadata_create(const struct stat* stats); 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 to_disk(const char* path, const void* data, unsigned long long data_size);
bool file_save_to_disk(const char* root_directory, File* file, const Config* config); bool file_save_to_disk(const char* root_directory, File* file);
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);
+1 -16
View File
@@ -5,17 +5,12 @@
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
static LogLevel current_log_level = LOG_LEVEL_WARNING; static LogLevel current_log_level = LOG_LEVEL_WARNING;
static FILE* log_fp = NULL;
void set_log_level(LogLevel level) { void set_log_level(LogLevel level) {
current_log_level = level; current_log_level = level;
} }
void log_set_file(FILE* fp) { void log_message(LogLevel log_level, char* format, ...) {
log_fp = fp;
}
void log_message(LogLevel log_level, const char* format, ...) {
if (log_level < current_log_level) if (log_level < current_log_level)
return; return;
time_t now = time(NULL); time_t now = time(NULL);
@@ -29,14 +24,4 @@ void log_message(LogLevel log_level, const char* format, ...) {
vfprintf(stderr, format, args); vfprintf(stderr, format, args);
va_end(args); va_end(args);
fprintf(stderr, "\n"); fprintf(stderr, "\n");
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);
vfprintf(log_fp, format, args);
va_end(args);
fprintf(log_fp, "\n");
fflush(log_fp);
}
} }
+1 -4
View File
@@ -1,12 +1,9 @@
#ifndef LOG_H #ifndef LOG_H
#define LOG_H #define LOG_H
#include <stdio.h>
typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel;
void log_message(LogLevel log_level, const char* message, ...); void log_message(LogLevel log_level, char* message, ...);
void set_log_level(LogLevel level); void set_log_level(LogLevel level);
void log_set_file(FILE* fp);
#endif #endif
+43 -109
View File
@@ -4,103 +4,67 @@
#include "protocol.h" #include "protocol.h"
#include <errno.h> #include <errno.h>
#include <fcntl.h> #include <fcntl.h>
#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
/*
* Wire format serialization (protocol version 2.0.0+):
* All metadata fields are serialized as fixed-width integers (int32_t / int64_t)
* to ensure cross-platform binary compatiblity. See metadata.h for the
* exact wire layout.
*
* Compile-time assertions verify that the native platform types fit within
* the chosen fixed-width representations.
*/
typedef char static_assert_mode_t_fits[(sizeof(mode_t) <= sizeof(int32_t)) ? 1 : -1];
typedef char static_assert_uid_t_fits[(sizeof(uid_t) <= sizeof(int32_t)) ? 1 : -1];
typedef char static_assert_gid_t_fits[(sizeof(gid_t) <= sizeof(int32_t)) ? 1 : -1];
void metadata_to_buf(char** buf, const FileMetadata* m) { void metadata_to_buf(char** buf, const FileMetadata* m) {
int32_t present = (m != NULL) ? 1 : 0; int present = (m != NULL) ? 1 : 0;
memcpy(*buf, &present, sizeof(present)); memcpy(*buf, &present, sizeof(int));
*buf += sizeof(present); *buf += sizeof(int);
if (m == NULL) if (m == NULL)
return; return;
int32_t mode = (int32_t)m->mode; memcpy(*buf, &m->mode, sizeof(mode_t));
memcpy(*buf, &mode, sizeof(mode)); *buf += sizeof(mode_t);
*buf += sizeof(mode); memcpy(*buf, &m->uid, sizeof(uid_t));
int32_t uid = (int32_t)m->uid; *buf += sizeof(uid_t);
memcpy(*buf, &uid, sizeof(uid)); memcpy(*buf, &m->gid, sizeof(gid_t));
*buf += sizeof(uid); *buf += sizeof(gid_t);
int32_t gid = (int32_t)m->gid; memcpy(*buf, &m->mtime_sec, sizeof(time_t));
memcpy(*buf, &gid, sizeof(gid)); *buf += sizeof(time_t);
*buf += sizeof(gid); memcpy(*buf, &m->mtime_nsec, sizeof(long));
int64_t mtime_sec = (int64_t)m->mtime_sec; *buf += sizeof(long);
memcpy(*buf, &mtime_sec, sizeof(mtime_sec));
*buf += sizeof(mtime_sec);
int64_t mtime_nsec = (int64_t)m->mtime_nsec;
memcpy(*buf, &mtime_nsec, sizeof(mtime_nsec));
*buf += sizeof(mtime_nsec);
} }
FileMetadata* metadata_from_buf(char** buf) { FileMetadata* metadata_from_buf(char** buf) {
int32_t present; int present;
memcpy(&present, *buf, sizeof(present)); memcpy(&present, *buf, sizeof(int));
*buf += sizeof(present); *buf += sizeof(int);
if (!present) if (!present)
return NULL; return NULL;
FileMetadata* m = malloc(sizeof(FileMetadata)); FileMetadata* m = malloc(sizeof(FileMetadata));
if (m == NULL) memcpy(&m->mode, *buf, sizeof(mode_t));
return NULL; *buf += sizeof(mode_t);
int32_t mode; memcpy(&m->uid, *buf, sizeof(uid_t));
memcpy(&mode, *buf, sizeof(mode)); *buf += sizeof(uid_t);
*buf += sizeof(mode); memcpy(&m->gid, *buf, sizeof(gid_t));
m->mode = (mode_t)mode; *buf += sizeof(gid_t);
int32_t uid; memcpy(&m->mtime_sec, *buf, sizeof(time_t));
memcpy(&uid, *buf, sizeof(uid)); *buf += sizeof(time_t);
*buf += sizeof(uid); memcpy(&m->mtime_nsec, *buf, sizeof(long));
m->uid = (uid_t)uid; *buf += sizeof(long);
int32_t gid;
memcpy(&gid, *buf, sizeof(gid));
*buf += sizeof(gid);
m->gid = (gid_t)gid;
int64_t mtime_sec;
memcpy(&mtime_sec, *buf, sizeof(mtime_sec));
*buf += sizeof(mtime_sec);
m->mtime_sec = (time_t)mtime_sec;
int64_t mtime_nsec;
memcpy(&mtime_nsec, *buf, sizeof(mtime_nsec));
*buf += sizeof(mtime_nsec);
m->mtime_nsec = (long)mtime_nsec;
return m; return m;
} }
bool metadata_send(int file_descriptor, FileMetadata* m) { bool metadata_send(int file_descriptor, FileMetadata* m) {
if (m == NULL) { if (m == NULL) {
int32_t zero = 0; int zero = 0;
return send_n_data(file_descriptor, &zero, sizeof(zero)); return send_n_data(file_descriptor, &zero, sizeof(int));
} }
int32_t present = 1; int present = 1;
int32_t mode = (int32_t)m->mode; return send_n_data(file_descriptor, &present, sizeof(int)) &&
int32_t uid = (int32_t)m->uid; send_n_data(file_descriptor, &m->mode, sizeof(mode_t)) &&
int32_t gid = (int32_t)m->gid; send_n_data(file_descriptor, &m->uid, sizeof(uid_t)) &&
int64_t mtime_sec = (int64_t)m->mtime_sec; send_n_data(file_descriptor, &m->gid, sizeof(gid_t)) &&
int64_t mtime_nsec = (int64_t)m->mtime_nsec; send_n_data(file_descriptor, &m->mtime_sec, sizeof(time_t)) &&
return send_n_data(file_descriptor, &present, sizeof(present)) && send_n_data(file_descriptor, &m->mtime_nsec, sizeof(long));
send_n_data(file_descriptor, &mode, sizeof(mode)) &&
send_n_data(file_descriptor, &uid, sizeof(uid)) &&
send_n_data(file_descriptor, &gid, sizeof(gid)) &&
send_n_data(file_descriptor, &mtime_sec, sizeof(mtime_sec)) &&
send_n_data(file_descriptor, &mtime_nsec, sizeof(mtime_nsec));
} }
FileMetadata* metadata_receive(int file_descriptor, int* ok) { FileMetadata* metadata_receive(int file_descriptor, int* ok) {
int32_t present; int present;
if (!receive_n_data(file_descriptor, &present, sizeof(present))) { if (!receive_n_data(file_descriptor, &present, sizeof(int))) {
if (ok) if (ok)
*ok = 0; *ok = 0;
return NULL; return NULL;
@@ -116,46 +80,16 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
*ok = 0; *ok = 0;
return NULL; return NULL;
} }
int32_t mode; if (!receive_n_data(file_descriptor, &m->mode, sizeof(mode_t)) ||
if (!receive_n_data(file_descriptor, &mode, sizeof(mode))) { !receive_n_data(file_descriptor, &m->uid, sizeof(uid_t)) ||
!receive_n_data(file_descriptor, &m->gid, sizeof(gid_t)) ||
!receive_n_data(file_descriptor, &m->mtime_sec, sizeof(time_t)) ||
!receive_n_data(file_descriptor, &m->mtime_nsec, sizeof(long))) {
free(m); free(m);
if (ok) if (ok)
*ok = 0; *ok = 0;
return NULL; return NULL;
} }
m->mode = (mode_t)mode;
int32_t uid;
if (!receive_n_data(file_descriptor, &uid, sizeof(uid))) {
free(m);
if (ok)
*ok = 0;
return NULL;
}
m->uid = (uid_t)uid;
int32_t gid;
if (!receive_n_data(file_descriptor, &gid, sizeof(gid))) {
free(m);
if (ok)
*ok = 0;
return NULL;
}
m->gid = (gid_t)gid;
int64_t mtime_sec;
if (!receive_n_data(file_descriptor, &mtime_sec, sizeof(mtime_sec))) {
free(m);
if (ok)
*ok = 0;
return NULL;
}
m->mtime_sec = (time_t)mtime_sec;
int64_t mtime_nsec;
if (!receive_n_data(file_descriptor, &mtime_nsec, sizeof(mtime_nsec))) {
free(m);
if (ok)
*ok = 0;
return NULL;
}
m->mtime_nsec = (long)mtime_nsec;
if (ok) if (ok)
*ok = 1; *ok = 1;
return m; return m;
@@ -164,7 +98,7 @@ FileMetadata* metadata_receive(int file_descriptor, int* ok) {
void file_restore_metadata(const char* path, FileMetadata* metadata) { void file_restore_metadata(const char* path, 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) != 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) if (chown(path, metadata->uid, metadata->gid) != 0)
log_message(LOG_LEVEL_WARNING, "Failed to chown %s: %s", path, strerror(errno)); log_message(LOG_LEVEL_WARNING, "Failed to chown %s: %s", path, strerror(errno));
+2 -19
View File
@@ -3,27 +3,10 @@
#include "file.h" #include "file.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h>
#include <sys/stat.h> #include <sys/stat.h>
/* #define FILE_METADATA_WIRE_SIZE \
* Wire format (introduced in protocol version 2.0.0): (sizeof(mode_t) + sizeof(uid_t) + sizeof(gid_t) + sizeof(time_t) + sizeof(long))
* int32_t present
* int32_t mode (was mode_t, platform-dependent)
* int32_t uid (was uid_t, platform-dependent)
* int32_t gid (was gid_t, platform-dependent)
* int64_t mtime_sec (was time_t, platform-dependent)
* int64_t mtime_nsec (was long, platform-dependent)
*
* Prior to 2.0.0 the wire format used the raw platform-dependent types,
* which broke compatiblity across different systems. All fields are now
* serialized as fixed-width integers.
*/
/* Size of metadata fields on wire, excluding the int32_t `present` field that
* is always sent first. The total wire size for present metadata is
* sizeof(int32_t) + FILE_METADATA_WIRE_SIZE (32 bytes on most platforms). */
#define FILE_METADATA_WIRE_SIZE (sizeof(int32_t) * 3 + sizeof(int64_t) * 2)
void metadata_to_buf(char** buf, const FileMetadata* m); void metadata_to_buf(char** buf, const FileMetadata* m);
FileMetadata* metadata_from_buf(char** buf); FileMetadata* metadata_from_buf(char** buf);
+2 -40
View File
@@ -105,16 +105,7 @@ int receive_thread(void* pipeline_context) {
Status status; Status status;
if (!receive_status(file_descriptor, &status)) if (!receive_status(file_descriptor, &status))
return thrd_error; return thrd_error;
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK || while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) {
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH) {
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) { if (status == STATUS_CHECK) {
bool skipped; bool skipped;
File* file = receive_incremental_check(file_descriptor, config, &skipped); File* file = receive_incremental_check(file_descriptor, config, &skipped);
@@ -126,34 +117,6 @@ int receive_thread(void* pipeline_context) {
} }
} else if (status == STATUS_CHUNK) { } else if (status == STATUS_CHUNK) {
receive_chunk_enqueue(file_descriptor, context); receive_chunk_enqueue(file_descriptor, context);
} 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 && stat(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 { } else {
File* file = file_receive(config, file_descriptor); File* file = file_receive(config, file_descriptor);
if (file) { if (file) {
@@ -163,7 +126,6 @@ int receive_thread(void* pipeline_context) {
log_message(LOG_LEVEL_ERROR, "Failed to receive file"); log_message(LOG_LEVEL_ERROR, "Failed to receive file");
} }
} }
next:
if (!receive_status(file_descriptor, &status)) if (!receive_status(file_descriptor, &status))
return thrd_error; return thrd_error;
} }
@@ -194,7 +156,7 @@ int write_thread(void* pipeline_context) {
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);
file_destroy(file); file_destroy(file);
} }
} }
+14 -57
View File
@@ -2,27 +2,20 @@
#include "log.h" #include "log.h"
#include <errno.h> #include <errno.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <poll.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#include <unistd.h> #include <unistd.h>
#define MAX_DATA_SIZE (256ULL * 1024 * 1024) /* 256 MB max per message */
#define RECEIVE_TIMEOUT_SEC 60 /* 60 second per-message timeout */
#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 SSL* io_ssl; static SSL* io_ssl = NULL;
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;
static struct timespec bw_last_refill = {0, 0}; static struct timespec bw_last_refill = {0, 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) {
io_read_fd = read_fd; io_read_fd = read_fd;
io_write_fd = write_fd; io_write_fd = write_fd;
@@ -53,11 +46,12 @@ static void bw_throttle(size_t bytes_written) {
bw_tokens -= (long long)bytes_written; bw_tokens -= (long long)bytes_written;
if (bw_tokens < 0) { if (bw_tokens < 0) {
long long deficit_us = (long long)((double)(-bw_tokens) / io_bwlimit * 1000000.0); long long deficit_ns = (long long)((double)(-bw_tokens) / io_bwlimit * 1000000000.0);
if (deficit_us >= 1000) struct timespec sleep_time, remaining;
poll(NULL, 0, (int)(deficit_us / 1000)); sleep_time.tv_sec = deficit_ns / 1000000000LL;
else sleep_time.tv_nsec = deficit_ns % 1000000000LL;
usleep((useconds_t)deficit_us); while (nanosleep(&sleep_time, &remaining) < 0 && errno == EINTR)
sleep_time = remaining;
bw_tokens = 0; bw_tokens = 0;
clock_gettime(CLOCK_MONOTONIC, &bw_last_refill); clock_gettime(CLOCK_MONOTONIC, &bw_last_refill);
} }
@@ -85,11 +79,6 @@ bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
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) {
int ssl_err = SSL_get_error(io_ssl, (int)bytes_send);
if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ)
continue;
}
log_message(LOG_LEVEL_ERROR, "Could not send data"); log_message(LOG_LEVEL_ERROR, "Could not send data");
return false; return false;
} }
@@ -103,21 +92,8 @@ 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) {
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); int fd = io_fd(io_read_fd, file_descriptor);
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += RECEIVE_TIMEOUT_SEC;
size_t total_bytes_received = 0; size_t total_bytes_received = 0;
while (total_bytes_received < data_size) { while (total_bytes_received < data_size) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec > deadline.tv_nsec)) {
log_message(LOG_LEVEL_ERROR, "Receive timeout after %ds", RECEIVE_TIMEOUT_SEC);
return false;
}
ssize_t bytes_received; ssize_t bytes_received;
if (io_ssl) if (io_ssl)
bytes_received = bytes_received =
@@ -126,11 +102,6 @@ bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
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) {
int ssl_err = SSL_get_error(io_ssl, (int)bytes_received);
if (ssl_err == SSL_ERROR_WANT_WRITE || ssl_err == SSL_ERROR_WANT_READ)
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");
else else
@@ -161,18 +132,16 @@ static const char* status_to_string(Status status) {
return "DELTA_SIGNATURE"; return "DELTA_SIGNATURE";
case STATUS_DELTA_DATA: case STATUS_DELTA_DATA:
return "DELTA_DATA"; return "DELTA_DATA";
case STATUS_KEEPALIVE:
return "KEEPALIVE";
case STATUS_ABORT:
return "ABORT";
case STATUS_CHECK_BATCH:
return "CHECK_BATCH";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
} }
bool send_str(int file_descriptor, const char* data) { bool send_str(int file_descriptor, const char* data) {
if (data == NULL) {
log_message(LOG_LEVEL_ERROR, "send_str called with NULL data");
return false;
}
size_t size = strlen(data); size_t size = strlen(data);
if (!send_n_data(file_descriptor, &size, sizeof(size_t))) if (!send_n_data(file_descriptor, &size, sizeof(size_t)))
return false; return false;
@@ -186,9 +155,9 @@ char* receive_str(int file_descriptor) {
size_t size; size_t size;
if (!receive_n_data(file_descriptor, &size, sizeof(size_t))) if (!receive_n_data(file_descriptor, &size, sizeof(size_t)))
return NULL; return NULL;
if (size > MAX_DATA_SIZE) { if (size > MAX_STRING_SIZE) {
log_message(LOG_LEVEL_ERROR, "String size %zu exceeds maximum %llu", size, log_message(LOG_LEVEL_ERROR, "receive_str: size %zu exceeds maximum %zu", size,
(unsigned long long)MAX_DATA_SIZE); (size_t)MAX_STRING_SIZE);
return NULL; return NULL;
} }
char* data = (char*)malloc(size + 1); char* data = (char*)malloc(size + 1);
@@ -217,17 +186,6 @@ Data* receive_data(int file_descriptor) {
unsigned long long size = 0; unsigned long long size = 0;
if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long))) if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long)))
return NULL; return NULL;
if (size > MAX_DATA_SIZE) {
log_message(LOG_LEVEL_ERROR, "Data size %llu exceeds maximum %llu", size,
(unsigned long long)MAX_DATA_SIZE);
return NULL;
}
if (total_allocated_bytes + size > MAX_CONNECTION_MEMORY) {
log_message(LOG_LEVEL_ERROR, "Per-connection memory limit exceeded (%llu + %llu > %llu)",
(unsigned long long)total_allocated_bytes, size,
(unsigned long long)MAX_CONNECTION_MEMORY);
return NULL;
}
void* data = malloc((size_t)size); void* data = malloc((size_t)size);
if (data == NULL) if (data == NULL)
return NULL; return NULL;
@@ -235,7 +193,6 @@ Data* receive_data(int file_descriptor) {
free(data); free(data);
return NULL; return NULL;
} }
total_allocated_bytes += 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); return data_create(data, (size_t)size);
} }
+1 -4
View File
@@ -20,10 +20,7 @@ enum NET_STATUS {
STATUS_MANIFEST, STATUS_MANIFEST,
STATUS_CHECK, STATUS_CHECK,
STATUS_DELTA_SIGNATURE, STATUS_DELTA_SIGNATURE,
STATUS_DELTA_DATA, STATUS_DELTA_DATA
STATUS_KEEPALIVE,
STATUS_ABORT,
STATUS_CHECK_BATCH
}; };
void io_set_fds(int read_fd, int write_fd); void io_set_fds(int read_fd, int write_fd);
+20 -16
View File
@@ -67,7 +67,7 @@ static int parse_remote_dest(const char* dest, RemoteDest* r) {
return 0; return 0;
} }
Client* client_connect_ssh(const char* destination, int port, const char* server_path) { Client* client_connect_ssh(const char* destination, int port) {
RemoteDest r; RemoteDest r;
if (parse_remote_dest(destination, &r) != 0) { if (parse_remote_dest(destination, &r) != 0) {
fprintf(stderr, "Invalid remote destination: %s\n", destination); fprintf(stderr, "Invalid remote destination: %s\n", destination);
@@ -118,20 +118,16 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
if (sv[1] > 1) if (sv[1] > 1)
close(sv[1]); close(sv[1]);
size_t ssh_user_len; char ssh_user[512];
if (r.user && r.user[0] != '\0') if (r.user && r.user[0] != '\0')
ssh_user_len = strlen(r.user) + 1 + strlen(r.host) + 1; snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
else else
ssh_user_len = strlen(r.host) + 1; snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
char* ssh_user = malloc(ssh_user_len);
if (!ssh_user)
_exit(1);
if (r.user && r.user[0] != '\0')
snprintf(ssh_user, ssh_user_len, "%s@%s", r.user, r.host);
else
snprintf(ssh_user, ssh_user_len, "%s", r.host);
char* ssh_argv[16]; size_t ssh_argv_max = 32;
char** ssh_argv = calloc(ssh_argv_max, sizeof(char*));
if (ssh_argv == NULL)
_exit(1);
int ac = 0; int ac = 0;
char port_str[16]; char port_str[16];
ssh_argv[ac++] = "ssh"; ssh_argv[ac++] = "ssh";
@@ -142,15 +138,24 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
ssh_argv[ac++] = "-o"; ssh_argv[ac++] = "-o";
ssh_argv[ac++] = "ControlPath=~/.cache/fastsync-%r@%h:%p"; ssh_argv[ac++] = "ControlPath=~/.cache/fastsync-%r@%h:%p";
if (port > 0 && port != 22) { if (port > 0 && port != 22) {
if ((size_t)ac + 2 >= ssh_argv_max) {
free(ssh_argv);
_exit(1);
}
ssh_argv[ac++] = "-p"; ssh_argv[ac++] = "-p";
snprintf(port_str, sizeof(port_str), "%d", port); snprintf(port_str, sizeof(port_str), "%d", port);
ssh_argv[ac++] = port_str; ssh_argv[ac++] = port_str;
} }
if ((size_t)ac + 3 >= ssh_argv_max) {
free(ssh_argv);
_exit(1);
}
ssh_argv[ac++] = ssh_user; ssh_argv[ac++] = ssh_user;
ssh_argv[ac++] = (char*)(server_path ? server_path : "fastsync-server"); ssh_argv[ac++] = "fastsync-server";
ssh_argv[ac++] = "--stdio"; ssh_argv[ac++] = "--stdio";
ssh_argv[ac] = NULL; ssh_argv[ac] = NULL;
execvp("ssh", ssh_argv); execvp("ssh", ssh_argv);
free(ssh_argv);
perror("exec of ssh failed"); perror("exec of ssh failed");
ssize_t wret = write(exec_pipe[1], "x", 1); ssize_t wret = write(exec_pipe[1], "x", 1);
(void)wret; (void)wret;
@@ -168,8 +173,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
close(sv[0]); close(sv[0]);
waitpid(pid, NULL, 0); waitpid(pid, NULL, 0);
remote_dest_destroy(&r); remote_dest_destroy(&r);
fprintf(stderr, "Error: could not launch '%s --stdio' on remote\n", fprintf(stderr, "Error: could not launch 'fastsync-server --stdio' on remote\n");
server_path ? server_path : "fastsync-server");
return NULL; return NULL;
} }
@@ -182,7 +186,7 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
return NULL; return NULL;
} }
client->file_descriptor = sv[0]; client->file_descriptor = sv[0];
client->address.sin_family = AF_UNIX; client->address.ss_family = AF_UNIX;
client->address_length = 0; client->address_length = 0;
client->ssh_child_pid = pid; client->ssh_child_pid = pid;
client->ssl = NULL; client->ssl = NULL;
+1 -1
View File
@@ -3,6 +3,6 @@
#include "transport_tcp.h" #include "transport_tcp.h"
Client* client_connect_ssh(const char* destination, int port, const char* server_path); Client* client_connect_ssh(const char* destination, int port);
#endif #endif
+140 -71
View File
@@ -2,26 +2,36 @@
#include "log.h" #include "log.h"
#include "protocol.h" #include "protocol.h"
#include <arpa/inet.h> #include <arpa/inet.h>
#include <errno.h> #include <netdb.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/socket.h>
#include <sys/types.h>
#include <sys/wait.h> #include <sys/wait.h>
#include <unistd.h> #include <unistd.h>
static volatile sig_atomic_t g_active_connections = 0; bool set_socket_timeouts(int fd) {
struct timeval tv;
tv.tv_sec = 30;
tv.tv_usec = 0;
static void sigchld_handler(int sig) { int keepalive = 1;
(void)sig; if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) < 0) {
int saved_errno = errno; perror("Could not set SO_KEEPALIVE");
while (waitpid(-1, NULL, WNOHANG) > 0) { return false;
if (g_active_connections > 0)
g_active_connections--;
} }
errno = saved_errno; if (setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0) {
perror("Could not set SO_RCVTIMEO");
return false;
}
if (setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
perror("Could not set SO_SNDTIMEO");
return false;
}
return true;
} }
Server* server_create(int port) { Server* server_create(int port) {
@@ -30,14 +40,26 @@ Server* server_create(int port) {
perror("Could not allocate space for Server"); perror("Could not allocate space for Server");
return NULL; return NULL;
} }
memset(&server->address, 0, sizeof(server->address));
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0); // Try IPv6 first, fall back to IPv4
if (file_descriptor < 0) { int fd = socket(AF_INET6, SOCK_STREAM, 0);
if (fd < 0) {
fd = socket(AF_INET, SOCK_STREAM, 0);
}
if (fd < 0) {
perror("Could not create Socket!"); perror("Could not create Socket!");
free(server); free(server);
return NULL; return NULL;
} }
server->file_descriptor = file_descriptor;
if (!set_socket_timeouts(fd)) {
close(fd);
free(server);
return NULL;
}
server->file_descriptor = fd;
int opt = 1; int opt = 1;
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
perror("Error setting a socket option!"); perror("Error setting a socket option!");
@@ -46,21 +68,64 @@ Server* server_create(int port) {
return NULL; return NULL;
} }
server->address.sin_family = AF_INET; // Determine address family from the actual socket
server->address.sin_addr.s_addr = INADDR_ANY; struct sockaddr_storage* addr = &server->address;
server->address.sin_port = htons(port); socklen_t addr_len = sizeof(*addr);
server->address_length = sizeof(server->address); if (getsockname(fd, (struct sockaddr*)addr, &addr_len) == 0) {
server->ssl_ctx = NULL; // Use the family of the socket we actually created
server->max_connections = 100; }
server->active_connections = 0;
struct sockaddr_in* addr4 = (struct sockaddr_in*)addr;
struct sockaddr_in6* addr6 = (struct sockaddr_in6*)addr;
if (addr->ss_family == AF_INET6) {
addr6->sin6_family = AF_INET6;
addr6->sin6_addr = in6addr_any;
addr6->sin6_port = htons(port);
server->address_length = sizeof(struct sockaddr_in6);
} else {
addr4->sin_family = AF_INET;
addr4->sin_addr.s_addr = INADDR_ANY;
addr4->sin_port = htons(port);
server->address_length = sizeof(struct sockaddr_in);
}
if (bind(server->file_descriptor, (struct sockaddr*)&server->address, server->address_length) < if (bind(server->file_descriptor, (struct sockaddr*)&server->address, server->address_length) <
0) { 0) {
// If IPv6 bind failed (maybe no IPv6), try IPv4
if (addr->ss_family == AF_INET6) {
close(fd);
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
perror("Could not create IPv4 Socket!");
free(server);
return NULL;
}
if (!set_socket_timeouts(fd)) {
close(fd);
free(server);
return NULL;
}
server->file_descriptor = fd;
setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(addr, 0, sizeof(*addr));
addr4->sin_family = AF_INET;
addr4->sin_addr.s_addr = INADDR_ANY;
addr4->sin_port = htons(port);
server->address_length = sizeof(struct sockaddr_in);
if (bind(server->file_descriptor, (struct sockaddr*)addr, server->address_length) < 0) {
perror("Could not bind server"); perror("Could not bind server");
close(server->file_descriptor); close(server->file_descriptor);
free(server); free(server);
return NULL; return NULL;
} }
} else {
perror("Could not bind server");
close(server->file_descriptor);
free(server);
return NULL;
}
}
return server; return server;
} }
@@ -83,21 +148,16 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
perror("Could not listen on port!"); perror("Could not listen on port!");
return; return;
} }
signal(SIGCHLD, sigchld_handler); signal(SIGCHLD, SIG_IGN);
while (1) { while (1) {
struct sockaddr_in client_addr; struct sockaddr_storage client_addr;
socklen_t client_len = sizeof(client_addr); socklen_t client_len = sizeof(client_addr);
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len); int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
if (fd < 0) { if (fd < 0) {
perror("Could not accept the connection"); perror("Could not accept the connection");
continue; continue;
} }
if ((unsigned int)g_active_connections >= server->max_connections) { set_socket_timeouts(fd);
log_message(LOG_LEVEL_WARNING, "Max connections (%u) reached, rejecting",
server->max_connections);
close(fd);
continue;
}
log_message(LOG_LEVEL_INFO, "%s", log_fmt); log_message(LOG_LEVEL_INFO, "%s", log_fmt);
pid_t pid = fork(); pid_t pid = fork();
if (pid == 0) { if (pid == 0) {
@@ -105,8 +165,6 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
child_fn(fd, child_ctx); child_fn(fd, child_ctx);
close(fd); close(fd);
_exit(0); _exit(0);
} else if (pid > 0) {
g_active_connections++;
} }
close(fd); close(fd);
} }
@@ -121,7 +179,11 @@ static void plain_child_fn(int fd, void* ctx) {
} }
bool server_listen(Server* server, void (*handler)(int file_descriptor)) { bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d", ntohs(server->address.sin_port)); struct sockaddr_in* addr4 = (struct sockaddr_in*)&server->address;
int port = (server->address.ss_family == AF_INET6)
? ntohs(((struct sockaddr_in6*)&server->address)->sin6_port)
: ntohs(addr4->sin_port);
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d", port);
struct plain_ctx ctx = {handler}; struct plain_ctx ctx = {handler};
accept_loop(server, plain_child_fn, &ctx, "Received Connection"); accept_loop(server, plain_child_fn, &ctx, "Received Connection");
return true; return true;
@@ -129,43 +191,23 @@ bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx, void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
const char* log_fmt) { const char* log_fmt) {
log_message(LOG_LEVEL_INFO, "Start TLS Listening on Port: %d", ntohs(server->address.sin_port)); struct sockaddr_in* addr4 = (struct sockaddr_in*)&server->address;
int port = (server->address.ss_family == AF_INET6)
? ntohs(((struct sockaddr_in6*)&server->address)->sin6_port)
: ntohs(addr4->sin_port);
log_message(LOG_LEVEL_INFO, "Start TLS Listening on Port: %d", port);
accept_loop(server, child_fn, child_ctx, log_fmt); accept_loop(server, child_fn, child_ctx, log_fmt);
} }
static int g_timeout_sec = 30;
static int g_contimeout_sec = 10;
void tcp_set_timeouts(int timeout_sec, int contimeout_sec) {
if (timeout_sec > 0)
g_timeout_sec = timeout_sec;
if (contimeout_sec > 0)
g_contimeout_sec = contimeout_sec;
}
static void tcp_apply_socket_timeout(int fd) {
struct timeval tv;
tv.tv_sec = g_timeout_sec;
tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
}
Client* client_create() { Client* client_create() {
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
if (file_descriptor < 0) {
perror("Could not create Socket!");
return NULL;
}
Client* client = (Client*)malloc(sizeof(Client)); Client* client = (Client*)malloc(sizeof(Client));
if (client == NULL) { if (client == NULL) {
close(file_descriptor);
return NULL; return NULL;
} }
client->file_descriptor = file_descriptor; memset(&client->address, 0, sizeof(client->address));
client->address.sin_family = AF_INET; client->address.ss_family = AF_UNSPEC;
client->address_length = sizeof(client->address); client->address_length = sizeof(client->address);
client->file_descriptor = -1;
client->ssh_child_pid = -1; client->ssh_child_pid = -1;
client->ssl = NULL; client->ssl = NULL;
client->ssl_ctx = NULL; client->ssl_ctx = NULL;
@@ -173,28 +215,52 @@ Client* client_create() {
} }
bool client_connect(Client* client, char* host, int port) { bool client_connect(Client* client, char* host, int port) {
client->address.sin_port = htons(port); struct addrinfo hints, *res, *rp;
client->address.sin_family = AF_INET; memset(&hints, 0, sizeof(hints));
client->address_length = sizeof(client->address); hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) { char port_str[16];
perror("Could not convert host address!"); snprintf(port_str, sizeof(port_str), "%d", port);
int gai_err = getaddrinfo(host, port_str, &hints, &res);
if (gai_err != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(gai_err));
return false; return false;
} }
struct timeval ct; // Try IPv6 first, then IPv4
ct.tv_sec = g_contimeout_sec; int fd = -1;
ct.tv_usec = 0; for (rp = res; rp != NULL; rp = rp->ai_next) {
setsockopt(client->file_descriptor, SOL_SOCKET, SO_RCVTIMEO, &ct, sizeof(ct)); fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
setsockopt(client->file_descriptor, SOL_SOCKET, SO_SNDTIMEO, &ct, sizeof(ct)); if (fd < 0)
continue;
if (!set_socket_timeouts(fd)) {
close(fd);
fd = -1;
continue;
}
if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
if (connect(client->file_descriptor, (struct sockaddr*)&client->address, client->address_length) < if (fd < 0) {
0) {
perror("Could not connect to Server!"); perror("Could not connect to Server!");
freeaddrinfo(res);
return false; return false;
} }
tcp_apply_socket_timeout(client->file_descriptor); // Save the connected address
memcpy(&client->address, rp->ai_addr, rp->ai_addrlen);
client->address_length = rp->ai_addrlen;
freeaddrinfo(res);
// Close old fd if any and set new one
if (client->file_descriptor >= 0)
close(client->file_descriptor);
client->file_descriptor = fd;
return true; return true;
} }
@@ -205,7 +271,10 @@ void client_disconnect(Client* client) {
client->ssl = NULL; client->ssl = NULL;
io_set_ssl(NULL); io_set_ssl(NULL);
} }
if (client->file_descriptor >= 0) {
close(client->file_descriptor); close(client->file_descriptor);
client->file_descriptor = -1;
}
if (client->ssh_child_pid > 0) { if (client->ssh_child_pid > 0) {
int status; int status;
waitpid(client->ssh_child_pid, &status, 0); waitpid(client->ssh_child_pid, &status, 0);
+5 -5
View File
@@ -1,21 +1,21 @@
#ifndef TRANSPORT_TCP_H #ifndef TRANSPORT_TCP_H
#define TRANSPORT_TCP_H #define TRANSPORT_TCP_H
#include <netdb.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <stdbool.h> #include <stdbool.h>
#include <sys/socket.h>
#include <sys/types.h> #include <sys/types.h>
typedef struct Server { typedef struct Server {
struct sockaddr_in address; struct sockaddr_storage address;
unsigned int address_length; unsigned int address_length;
int file_descriptor; int file_descriptor;
void* ssl_ctx; void* ssl_ctx;
unsigned int max_connections;
volatile unsigned int active_connections;
} Server; } Server;
typedef struct Client { typedef struct Client {
struct sockaddr_in address; struct sockaddr_storage address;
unsigned int address_length; unsigned int address_length;
int file_descriptor; int file_descriptor;
pid_t ssh_child_pid; pid_t ssh_child_pid;
@@ -32,6 +32,6 @@ Client* client_create();
bool client_connect(Client* client, char* host, int port); bool client_connect(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); bool set_socket_timeouts(int fd);
#endif #endif
+20 -9
View File
@@ -5,6 +5,8 @@
#include <arpa/inet.h> #include <arpa/inet.h>
#include <openssl/err.h> #include <openssl/err.h>
#include <openssl/ssl.h> #include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <signal.h> #include <signal.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -73,6 +75,10 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
SSL_CTX_set_verify_depth(ctx, 4); SSL_CTX_set_verify_depth(ctx, 4);
} else { } else {
if (!is_server) {
log_message(LOG_LEVEL_WARNING,
"No CA path provided — TLS server certificate will not be verified");
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL);
} }
@@ -86,6 +92,7 @@ static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server) {
return NULL; return NULL;
} }
SSL_set_fd(ssl, fd); SSL_set_fd(ssl, fd);
int ret; int ret;
if (is_server) if (is_server)
ret = SSL_accept(ssl); ret = SSL_accept(ssl);
@@ -98,6 +105,17 @@ static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server) {
SSL_free(ssl); SSL_free(ssl);
return NULL; return NULL;
} }
// In client mode, check verification result if peer verification was requested
if (!is_server) {
long verify_result = SSL_get_verify_result(ssl);
if (verify_result != X509_V_OK) {
log_message(LOG_LEVEL_ERROR, "TLS certificate verification failed: %ld", verify_result);
SSL_free(ssl);
return NULL;
}
}
return ssl; return ssl;
} }
@@ -135,16 +153,9 @@ 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) {
client->address.sin_port = htons(port); // Use the common TCP connection logic (with IPv6 support)
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) { if (!client_connect(client, host, port))
perror("Could not convert host address!");
return false; return false;
}
if (connect(client->file_descriptor, (struct sockaddr*)&client->address, client->address_length) <
0) {
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)
+28 -58
View File
@@ -10,19 +10,23 @@
#include <unistd.h> #include <unistd.h>
bool mkdir_r(const char* path) { bool mkdir_r(const char* path) {
char* path_duplicate = malloc(strlen(path) + 1); size_t path_len = strlen(path);
char* path_duplicate = malloc(path_len + 1);
if (!path_duplicate) if (!path_duplicate)
return false; return false;
strcpy(path_duplicate, path); memcpy(path_duplicate, path, path_len + 1);
char* path_current = (char*)malloc((strlen(path) + 2) * sizeof(char)); /* Buffer for building subpaths: path_len + 1 for leading '/' + 1 for null */
size_t buf_size = path_len + 2;
char* path_current = (char*)malloc(buf_size);
if (!path_current) { if (!path_current) {
free(path_duplicate); free(path_duplicate);
return false; return false;
} }
char* path_current_position = path_current; size_t pos = 0;
if (path[0] == '/') { if (path[0] == '/') {
strcpy(path_current, "/"); path_current[0] = '/';
path_current_position += 1; path_current[1] = '\0';
pos = 1;
} else { } else {
path_current[0] = '\0'; path_current[0] = '\0';
} }
@@ -31,10 +35,16 @@ bool mkdir_r(const char* path) {
const char* part = strtok_r(path_duplicate, delimiter, &saveptr); const char* part = strtok_r(path_duplicate, delimiter, &saveptr);
bool ok = true; bool ok = true;
while (part != NULL) { while (part != NULL) {
strcpy(path_current_position, part); size_t part_len = strlen(part);
path_current_position += strlen(part) * sizeof(char); if (pos + part_len + 1 >= buf_size) {
strcpy(path_current_position, "/"); ok = false;
path_current_position += sizeof(char); break;
}
memcpy(path_current + pos, part, part_len);
pos += part_len;
path_current[pos] = '/';
pos++;
path_current[pos] = '\0';
struct stat st; struct stat st;
if (stat(path_current, &st) != 0) { if (stat(path_current, &st) != 0) {
if (mkdir(path_current, 0755) != 0) { if (mkdir(path_current, 0755) != 0) {
@@ -49,7 +59,6 @@ bool mkdir_r(const char* path) {
free(path_current); free(path_current);
return ok; return ok;
} }
char* str_dup(const char* string) { char* str_dup(const char* string) {
if (string == NULL) if (string == NULL)
return NULL; return NULL;
@@ -61,19 +70,6 @@ char* str_dup(const char* string) {
bool glob_match(const char* pattern, const char* str) { bool glob_match(const char* pattern, const char* str) {
while (*pattern) { while (*pattern) {
if (*pattern == '*') { if (*pattern == '*') {
if (*(pattern + 1) == '*') {
pattern += 2;
if (*pattern == '\0')
return true;
if (*pattern == '/')
pattern++;
while (*str) {
if (glob_match(pattern, str))
return true;
str++;
}
return glob_match(pattern, str);
}
pattern++; pattern++;
while (*str && *str != '/') { while (*str && *str != '/') {
if (glob_match(pattern, str)) if (glob_match(pattern, str))
@@ -87,15 +83,8 @@ bool glob_match(const char* pattern, const char* str) {
pattern++; pattern++;
str++; str++;
} else { } else {
if (*pattern != *str) { if (*pattern != *str)
if (*pattern == '/' && *(pattern + 1) == '*' && *(pattern + 2) == '*') {
const char* rest = pattern + 3;
if (*rest == '/')
rest++;
return glob_match(rest, str);
}
return false; return false;
}
pattern++; pattern++;
str++; str++;
} }
@@ -119,7 +108,7 @@ static void delete_extras_walk(const char* abs_path, const char* rel_path, Array
if (!dir) if (!dir)
return; return;
bool all_removed = true; bool all_removed = true;
const struct dirent* entry; 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;
@@ -169,37 +158,18 @@ void delete_extras(const char* dest_root, ArrayList* manifest) {
delete_extras_walk(dest_root, "", manifest); delete_extras_walk(dest_root, "", manifest);
} }
bool has_path_traversal(const char* path) { char* path_cat(const char* path1, char* path2) {
if (!path)
return false;
char* dup = str_dup(path);
if (!dup)
return false;
char* saveptr;
const char* part = strtok_r(dup, "/", &saveptr);
while (part) {
if (strcmp(part, "..") == 0) {
free(dup);
return true;
}
part = strtok_r(NULL, "/", &saveptr);
}
free(dup);
return false;
}
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);
if (path2 == NULL || *path2 == '\0') if (path2 == NULL || *path2 == '\0')
return str_dup(path1); return str_dup(path1);
size_t path1_len = strlen(path1); int path1_len = strlen(path1);
size_t path2_len = strlen(path2); int path2_len = strlen(path2);
size_t offset = 0; char* path2_pointer = path2;
if (path1[path1_len - 1] == '/') if (path1[path1_len - 1] == '/')
path1_len -= 1; path1_len -= 1;
if (path2[0] == '/') { if (path2[0] == '/') {
offset = 1; path2_pointer += 1;
path2_len -= 1; path2_len -= 1;
} }
char* new_path = malloc(path1_len + path2_len + 2); char* new_path = malloc(path1_len + path2_len + 2);
@@ -207,7 +177,7 @@ char* path_cat(const char* path1, const char* path2) {
return NULL; return NULL;
memcpy(new_path, path1, path1_len); memcpy(new_path, path1, path1_len);
new_path[path1_len] = '/'; new_path[path1_len] = '/';
memcpy(new_path + path1_len + 1, path2 + offset, path2_len); memcpy(new_path + path1_len + 1, path2_pointer, path2_len);
new_path[path1_len + path2_len + 1] = '\0'; new_path[path1_len + path2_len + 1] = '\0';
return new_path; return new_path;
} }
+1 -2
View File
@@ -6,9 +6,8 @@
bool mkdir_r(const char* path); 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, 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); void delete_extras(const char* dest_root, ArrayList* manifest);
bool has_path_traversal(const char* path);
#endif #endif
-12
View File
@@ -5,11 +5,8 @@
#include "test_data.h" #include "test_data.h"
#include "test_delta.h" #include "test_delta.h"
#include "test_file.h" #include "test_file.h"
#include "test_file_sendfile.h"
#include "test_glob.h" #include "test_glob.h"
#include "test_log.h"
#include "test_metadata.h" #include "test_metadata.h"
#include "test_multiprocessing.h"
#include "test_property.h" #include "test_property.h"
#include "test_protocol.h" #include "test_protocol.h"
#include "test_queue.h" #include "test_queue.h"
@@ -17,9 +14,6 @@
#include "test_scanner.h" #include "test_scanner.h"
#include "test_shared_utils.h" #include "test_shared_utils.h"
#include "test_stress.h" #include "test_stress.h"
#include "test_transport_tcp.h"
#include "test_transport_ssh.h"
#include "test_transport_tls.h"
#include "test_utils.h" #include "test_utils.h"
#include <stdio.h> #include <stdio.h>
@@ -44,15 +38,9 @@ int main() {
RUN_TEST(test_metadata); RUN_TEST(test_metadata);
RUN_TEST(test_glob); RUN_TEST(test_glob);
RUN_TEST(test_file); RUN_TEST(test_file);
RUN_TEST(test_file_sendfile);
RUN_TEST(test_multiprocessing);
RUN_TEST(test_log);
RUN_TEST(test_robustness); RUN_TEST(test_robustness);
RUN_TEST(test_stress); RUN_TEST(test_stress);
RUN_TEST(test_property); RUN_TEST(test_property);
RUN_TEST(test_transport_tcp);
RUN_TEST(test_transport_ssh);
RUN_TEST(test_transport_tls);
printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n"); printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n");
printf("Total Tests Run: %d\n", tests_run); printf("Total Tests Run: %d\n", tests_run);
+12 -51
View File
@@ -7,17 +7,9 @@
#include <stdlib.h> #include <stdlib.h>
static void test_config_lifecycle() { static void test_config_lifecycle() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/dst"), true, true, false,
false, false, 1, false, 0);
EXPECT_NOT_NULL(cfg); EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("1.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
cfg->save_to_disk = true;
cfg->use_multithreading = true;
cfg->use_chunk_serialization = false;
cfg->use_compression = false;
cfg->compression_level = 1;
EXPECT_EQ_STR(cfg->version, "1.0"); EXPECT_EQ_STR(cfg->version, "1.0");
EXPECT_EQ_STR(cfg->send_directory, "/src"); EXPECT_EQ_STR(cfg->send_directory, "/src");
EXPECT_EQ_STR(cfg->receive_root_directory, "/dst"); EXPECT_EQ_STR(cfg->receive_root_directory, "/dst");
@@ -31,14 +23,9 @@ static void test_config_lifecycle() {
} }
static void test_config_ssh_dest() { static void test_config_ssh_dest() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("user@host:/dst"), true,
false, false, false, false, 1, false, 0);
EXPECT_NOT_NULL(cfg); EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("1.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("user@host:/dst");
cfg->save_to_disk = true;
cfg->compression_level = 1;
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP); EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
EXPECT_NULL(cfg->ssh_destination); EXPECT_NULL(cfg->ssh_destination);
EXPECT_EQ_STR(cfg->receive_root_directory, "user@host:/dst"); EXPECT_EQ_STR(cfg->receive_root_directory, "user@host:/dst");
@@ -51,14 +38,8 @@ static void test_config_ssh_dest() {
} }
static void test_config_ssh_dest_local_path() { static void test_config_ssh_dest_local_path() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/local/path"), true, false,
EXPECT_NOT_NULL(cfg); false, false, false, 1, false, 0);
free(cfg->version);
cfg->version = str_dup("1.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/local/path");
cfg->save_to_disk = true;
cfg->compression_level = 1;
config_parse_ssh_dest(cfg); config_parse_ssh_dest(cfg);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP); EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
EXPECT_NULL(cfg->ssh_destination); EXPECT_NULL(cfg->ssh_destination);
@@ -67,14 +48,8 @@ static void test_config_ssh_dest_local_path() {
} }
static void test_config_ssh_dest_no_user() { static void test_config_ssh_dest_no_user() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("host:/remote"), true, false,
EXPECT_NOT_NULL(cfg); false, false, false, 1, false, 0);
free(cfg->version);
cfg->version = str_dup("1.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("host:/remote");
cfg->save_to_disk = true;
cfg->compression_level = 1;
config_parse_ssh_dest(cfg); config_parse_ssh_dest(cfg);
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH); EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
EXPECT_EQ_STR(cfg->ssh_destination, "host:/remote"); EXPECT_EQ_STR(cfg->ssh_destination, "host:/remote");
@@ -83,14 +58,8 @@ static void test_config_ssh_dest_no_user() {
} }
static void test_pipeline_sender_lifecycle() { static void test_pipeline_sender_lifecycle() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("2.0"), str_dup("/src2"), str_dup("/dst2"), false, false,
free(cfg->version); true, true, false, 1, false, 0);
cfg->version = str_dup("2.0");
cfg->send_directory = str_dup("/src2");
cfg->receive_root_directory = str_dup("/dst2");
cfg->use_chunk_serialization = true;
cfg->use_compression = true;
cfg->compression_level = 1;
Queue* q1 = queue_create(5, NULL); Queue* q1 = queue_create(5, NULL);
Queue* q2 = queue_create(15, NULL); Queue* q2 = queue_create(15, NULL);
@@ -106,16 +75,8 @@ static void test_pipeline_sender_lifecycle() {
} }
static void test_pipeline_receiver_lifecycle() { static void test_pipeline_receiver_lifecycle() {
Config* cfg = config_create(); Config* cfg = config_create(str_dup("3.0"), str_dup("/src3"), str_dup("/dst3"), true, true, true,
free(cfg->version); true, false, 1, false, 0);
cfg->version = str_dup("3.0");
cfg->send_directory = str_dup("/src3");
cfg->receive_root_directory = str_dup("/dst3");
cfg->save_to_disk = true;
cfg->use_multithreading = true;
cfg->use_chunk_serialization = true;
cfg->use_compression = true;
cfg->compression_level = 1;
Queue* q = queue_create(20, NULL); Queue* q = queue_create(20, NULL);
PipelineContextReceiver* pcr = pipeline_context_receiver_create(cfg, q, 42); PipelineContextReceiver* pcr = pipeline_context_receiver_create(cfg, q, 42);
+8 -5
View File
@@ -69,7 +69,7 @@ static void test_file_save_to_disk() {
memcpy(f->data->data, content, strlen(content)); memcpy(f->data->data, content, strlen(content));
f->data->size = strlen(content); f->data->size = strlen(content);
EXPECT_TRUE(file_save_to_disk("test_save_tmp", f, NULL)); EXPECT_TRUE(file_save_to_disk("test_save_tmp", f));
struct stat st; struct stat st;
EXPECT_EQ_INT(stat("test_save_tmp/saved_file.txt", &st), 0); EXPECT_EQ_INT(stat("test_save_tmp/saved_file.txt", &st), 0);
@@ -154,9 +154,8 @@ static void test_file_send_receive() {
memcpy(file->data->data, content, len); memcpy(file->data->data, content, len);
file->data->size = len; file->data->size = len;
Config* cfg = config_create(); Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
cfg->send_directory = str_dup("/tmp"); false, false, false, false, 0, false, 0);
cfg->receive_root_directory = str_dup("/tmp");
int p[2]; int p[2];
EXPECT_EQ_INT(pipe(p), 0); EXPECT_EQ_INT(pipe(p), 0);
@@ -218,6 +217,10 @@ static void test_file_send_no_path() {
pid_t pid = fork(); pid_t pid = fork();
if (pid == 0) { if (pid == 0) {
close(p[1]); close(p[1]);
// Read file type indicator
int file_type;
EXPECT_TRUE(receive_int(p[0], &file_type));
EXPECT_EQ_INT(file_type, (int)FILE_TYPE_REGULAR);
Data* received = receive_data(p[0]); Data* received = receive_data(p[0]);
close(p[0]); close(p[0]);
@@ -272,7 +275,7 @@ void test_file() {
test_to_disk_basic(); test_to_disk_basic();
test_to_disk_creates_dirs(); test_to_disk_creates_dirs();
test_file_content_to_buffer(); test_file_content_to_buffer();
if (!is_running_under_valgrind()) { if (!getenv("FASTSYNC_UNDER_VALGRIND")) {
// Fork tests are skipped under valgrind because the parent process runs // Fork tests are skipped under valgrind because the parent process runs
// orders of magnitude slower than the child (parent is instrumented, child // orders of magnitude slower than the child (parent is instrumented, child
// is not), which causes pipe-based protocol handshake timeouts. The parent // is not), which causes pipe-based protocol handshake timeouts. The parent
-276
View File
@@ -1,276 +0,0 @@
#include "test_file_sendfile.h"
#include "file.h"
#include "config.h"
#include "protocol.h"
#include "utils.h"
#include "test_utils.h"
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
/* Test basic sendfile transfer: create a file, send it via file_send_sendfile,
* receive via file_receive, and verify contents. */
static void test_sendfile_basic() {
const char* content = "Hello from sendfile test!";
size_t len = strlen(content);
EXPECT_TRUE(to_disk("test_sendfile_basic.txt", content, len));
File* file = file_create("test_sendfile_basic.txt");
EXPECT_NOT_NULL(file);
/* Set the size so file_send_sendfile can report it */
file->data->size = len;
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->send_directory = str_dup("/tmp");
cfg->receive_root_directory = str_dup("/tmp");
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
/* Child: receive */
close(p[1]);
File* received = file_receive(cfg, p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else {
if (!received->path || strcmp(received->path, "test_sendfile_basic.txt") != 0)
ok = false;
if (!received->data || received->data->size != len)
ok = false;
else if (memcmp(received->data->data, content, len) != 0)
ok = false;
}
file_destroy(received);
config_delete(cfg);
_exit(ok ? 0 : 1);
} else {
/* Parent: send via sendfile */
close(p[0]);
bool sent = file_send_sendfile(file, p[1], false, 0, true);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
config_delete(cfg);
unlink("test_sendfile_basic.txt");
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
/* Test sendfile with an empty file */
static void test_sendfile_empty_file() {
const char* content = "";
size_t len = 0;
EXPECT_TRUE(to_disk("test_sendfile_empty.txt", content, len));
File* file = file_create("test_sendfile_empty.txt");
EXPECT_NOT_NULL(file);
file->data->size = 0;
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->send_directory = str_dup("/tmp");
cfg->receive_root_directory = str_dup("/tmp");
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
/* Child: receive */
close(p[1]);
File* received = file_receive(cfg, p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else {
if (strcmp(received->path, "test_sendfile_empty.txt") != 0)
ok = false;
if (received->data->size != 0)
ok = false;
}
file_destroy(received);
config_delete(cfg);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
bool sent = file_send_sendfile(file, p[1], false, 0, true);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
config_delete(cfg);
unlink("test_sendfile_empty.txt");
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
/* Test error path: file does not exist on disk */
static void test_sendfile_missing_file() {
File* file = file_create("nonexistent_sendfile_test_file.txt");
EXPECT_NOT_NULL(file);
file->data->size = 100; /* fake size */
/* Use a pipe that we can write to but sendfile should fail */
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
/* file_send_sendfile will try to open the nonexistent file -> should return false */
bool sent = file_send_sendfile(file, p[1], false, 0, true);
close(p[0]);
close(p[1]);
file_destroy(file);
EXPECT_FALSE(sent);
}
/* Test compression level > 0 falls back to file_send_single_calls */
static void test_sendfile_compression_fallback() {
const char* content = "Compression fallback content";
size_t len = strlen(content);
EXPECT_TRUE(to_disk("test_sendfile_comp.txt", content, len));
struct stat st;
EXPECT_EQ_INT(stat("test_sendfile_comp.txt", &st), 0);
File* file = file_create("test_sendfile_comp.txt");
EXPECT_NOT_NULL(file);
/* Load the file data into memory (required by file_send_single_calls fallback) */
file->data->size = (size_t)st.st_size;
EXPECT_TRUE(file_load_data(file));
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
cfg->send_directory = str_dup("/tmp");
cfg->receive_root_directory = str_dup("/tmp");
cfg->use_compression = true;
cfg->compression_level = 3;
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
/* Child: receive */
close(p[1]);
File* received = file_receive(cfg, p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else {
if (received->data->size != len)
ok = false;
else if (memcmp(received->data->data, content, len) != 0)
ok = false;
}
file_destroy(received);
config_delete(cfg);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
/* compression_level = 3 triggers fallback to file_send_single_calls */
bool sent = file_send_sendfile(file, p[1], false, 3, true);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
config_delete(cfg);
unlink("test_sendfile_comp.txt");
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
/* Test sendfile without path (send_path = false) */
static void test_sendfile_no_path() {
const char* content = "No path sendfile test";
size_t len = strlen(content);
EXPECT_TRUE(to_disk("test_sendfile_nopath.txt", content, len));
File* file = file_create("test_sendfile_nopath.txt");
EXPECT_NOT_NULL(file);
file->data->size = len;
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
pid_t pid = fork();
if (pid == 0) {
close(p[1]);
Data* received = receive_data(p[0]);
close(p[0]);
bool ok = true;
if (!received)
ok = false;
else if (received->size != len)
ok = false;
else if (memcmp(received->data, content, len) != 0)
ok = false;
data_destroy(received);
_exit(ok ? 0 : 1);
} else {
close(p[0]);
bool sent = file_send_sendfile(file, p[1], false, 0, false);
close(p[1]);
int status;
waitpid(pid, &status, 0);
file_destroy(file);
unlink("test_sendfile_nopath.txt");
EXPECT_TRUE(sent);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
}
void test_file_sendfile() {
if (!is_running_under_valgrind()) {
// Fork tests are skipped under valgrind because the parent process runs
// orders of magnitude slower than the child (parent is instrumented, child
// is not), which causes pipe-based protocol handshake timeouts. The parent
// process itself has zero valgrind errors -- the failures are all in the
// forked children where inherited allocations are reported as leaks.
test_sendfile_basic();
test_sendfile_empty_file();
test_sendfile_compression_fallback();
test_sendfile_no_path();
}
test_sendfile_missing_file(); // no fork, safe under valgrind
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_FILE_SENDFILE_H
#define TEST_FILE_SENDFILE_H
void test_file_sendfile();
#endif
-31
View File
@@ -49,33 +49,6 @@ static void test_glob_question_star() {
EXPECT_TRUE(glob_match("?*.txt", "a.txt")); EXPECT_TRUE(glob_match("?*.txt", "a.txt"));
} }
static void test_glob_doublestar_match_all() {
EXPECT_TRUE(glob_match("**", "anything"));
EXPECT_TRUE(glob_match("**", "path/to/file"));
}
static void test_glob_doublestar_prefix() {
EXPECT_TRUE(glob_match("**/foo", "foo"));
EXPECT_TRUE(glob_match("**/foo", "bar/foo"));
EXPECT_TRUE(glob_match("**/foo", "a/b/c/foo"));
EXPECT_FALSE(glob_match("**/foo", "foobar"));
EXPECT_FALSE(glob_match("**/foo", "bar/foobar"));
}
static void test_glob_doublestar_suffix() {
EXPECT_TRUE(glob_match("foo/**", "foo"));
EXPECT_TRUE(glob_match("foo/**", "foo/bar"));
EXPECT_TRUE(glob_match("foo/**", "foo/bar/baz"));
EXPECT_FALSE(glob_match("foo/**", "foobar"));
}
static void test_glob_doublestar_mid() {
EXPECT_TRUE(glob_match("a/**/b", "a/b"));
EXPECT_TRUE(glob_match("a/**/b", "a/x/b"));
EXPECT_TRUE(glob_match("a/**/b", "a/x/y/z/b"));
EXPECT_FALSE(glob_match("a/**/b", "a/x/bad"));
}
void test_glob() { void test_glob() {
test_glob_exact_match(); test_glob_exact_match();
test_glob_question_mark(); test_glob_question_mark();
@@ -87,8 +60,4 @@ void test_glob() {
test_glob_slash_not_matched(); test_glob_slash_not_matched();
test_glob_complex(); test_glob_complex();
test_glob_question_star(); test_glob_question_star();
test_glob_doublestar_match_all();
test_glob_doublestar_prefix();
test_glob_doublestar_suffix();
test_glob_doublestar_mid();
} }
-116
View File
@@ -1,116 +0,0 @@
#include "test_log.h"
#include "log.h"
#include "test_utils.h"
/* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not.
* We can't easily capture stderr in unit tests, so we verify the functions don't crash
* and that set_log_level changes behavior. */
static void test_log_message_debug() {
set_log_level(LOG_LEVEL_WARNING);
log_message(LOG_LEVEL_DEBUG, "debug message: %d", 42);
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_message_info() {
set_log_level(LOG_LEVEL_WARNING);
log_message(LOG_LEVEL_INFO, "info message: %s", "test");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_message_warning() {
set_log_level(LOG_LEVEL_WARNING);
log_message(LOG_LEVEL_WARNING, "warning message: %d %s", 1, "test");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_message_error() {
set_log_level(LOG_LEVEL_WARNING);
log_message(LOG_LEVEL_ERROR, "error message: %s", "critical");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_set_level_debug() {
set_log_level(LOG_LEVEL_DEBUG);
/* After setting to DEBUG, all levels should be shown */
log_message(LOG_LEVEL_DEBUG, "debug after set");
log_message(LOG_LEVEL_INFO, "info after set");
log_message(LOG_LEVEL_WARNING, "warning after set");
log_message(LOG_LEVEL_ERROR, "error after set");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_set_level_info() {
set_log_level(LOG_LEVEL_INFO);
/* INFO level should show INFO, WARNING, ERROR but not DEBUG */
log_message(LOG_LEVEL_DEBUG, "debug should be filtered"); /* filtered */
log_message(LOG_LEVEL_INFO, "info should show");
log_message(LOG_LEVEL_WARNING, "warning should show");
log_message(LOG_LEVEL_ERROR, "error should show");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
static void test_log_set_level_error() {
set_log_level(LOG_LEVEL_ERROR);
/* ERROR level: only ERROR should show */
log_message(LOG_LEVEL_DEBUG, "debug filtered");
log_message(LOG_LEVEL_INFO, "info filtered");
log_message(LOG_LEVEL_WARNING, "warning filtered");
log_message(LOG_LEVEL_ERROR, "error should show");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
/* Test that set_log_level with default WARNING filters correctly */
static void test_log_filtering() {
/* Reset to default */
set_log_level(LOG_LEVEL_WARNING);
/* These should be filtered */
log_message(LOG_LEVEL_DEBUG, "filtered debug");
log_message(LOG_LEVEL_INFO, "filtered info");
/* These should be shown */
log_message(LOG_LEVEL_WARNING, "visible warning");
log_message(LOG_LEVEL_ERROR, "visible error");
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
/* Test that log_message handles various format strings */
static void test_log_message_formats() {
set_log_level(LOG_LEVEL_DEBUG);
log_message(LOG_LEVEL_DEBUG, "simple string");
log_message(LOG_LEVEL_INFO, "integer: %d", -1);
log_message(LOG_LEVEL_WARNING, "string: %s", "hello");
log_message(LOG_LEVEL_ERROR, "multiple: %d %s %d", 1, "two", 3);
/* crash regression test — stderr capture would need infrastructure changes */
EXPECT_TRUE(true);
}
void test_log() {
test_log_message_debug();
test_log_message_info();
test_log_message_warning();
test_log_message_error();
test_log_set_level_debug();
test_log_set_level_info();
test_log_set_level_error();
test_log_filtering();
test_log_message_formats();
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_LOG_H
#define TEST_LOG_H
void test_log();
#endif
-118
View File
@@ -1,118 +0,0 @@
#include "test_multiprocessing.h"
#include "multiprocessing.h"
#include "config.h"
#include "queue.h"
#include "utils.h"
#include "test_utils.h"
#include <stdlib.h>
/* Test pipeline_context_sender_create/destroy with valid arguments */
static void test_sender_create_destroy() {
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("1.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
Queue* q_scanner = queue_create(5, NULL);
EXPECT_NOT_NULL(q_scanner);
Queue* q_loader = queue_create(10, NULL);
EXPECT_NOT_NULL(q_loader);
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q_scanner, q_loader);
EXPECT_NOT_NULL(ctx);
EXPECT_EQ_STR(ctx->config->version, "1.0");
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 5);
EXPECT_EQ_INT(ctx->queue_loader->capacity, 10);
EXPECT_FALSE(ctx->scanner_done);
EXPECT_FALSE(ctx->loader_done);
EXPECT_NULL(ctx->manifest);
pipeline_context_sender_destroy(ctx);
}
/* Test pipeline_context_receiver_create/destroy with valid arguments */
static void test_receiver_create_destroy() {
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("2.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
cfg->save_to_disk = true;
cfg->use_multithreading = true;
Queue* q = queue_create(20, NULL);
EXPECT_NOT_NULL(q);
PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 42);
EXPECT_NOT_NULL(ctx);
EXPECT_EQ_STR(ctx->config->version, "2.0");
EXPECT_EQ_INT(ctx->queue->capacity, 20);
EXPECT_EQ_INT(ctx->file_descriptor, 42);
EXPECT_FALSE(ctx->receiver_done);
pipeline_context_receiver_destroy(ctx);
}
/* Test that create handles various queue capacities */
static void test_sender_queue_capacities() {
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("3.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
/* Single-element queues */
Queue* q1 = queue_create(1, NULL);
Queue* q2 = queue_create(1, NULL);
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2);
EXPECT_NOT_NULL(ctx);
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 1);
EXPECT_EQ_INT(ctx->queue_loader->capacity, 1);
pipeline_context_sender_destroy(ctx);
}
/* Test that create handles zero-capacity queues */
static void test_sender_zero_capacity() {
Config* cfg = config_create();
EXPECT_NOT_NULL(cfg);
free(cfg->version);
cfg->version = str_dup("4.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
Queue* q1 = queue_create(0, NULL);
Queue* q2 = queue_create(0, NULL);
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2);
EXPECT_NOT_NULL(ctx);
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 0);
EXPECT_EQ_INT(ctx->queue_loader->capacity, 0);
pipeline_context_sender_destroy(ctx);
}
/* Test receiver with zero file_descriptor */
static void test_receiver_fd_zero() {
Config* cfg = config_create();
free(cfg->version);
cfg->version = str_dup("5.0");
cfg->send_directory = str_dup("/src");
cfg->receive_root_directory = str_dup("/dst");
Queue* q = queue_create(5, NULL);
PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 0);
EXPECT_NOT_NULL(ctx);
EXPECT_EQ_INT(ctx->file_descriptor, 0);
EXPECT_FALSE(ctx->receiver_done);
pipeline_context_receiver_destroy(ctx);
}
void test_multiprocessing() {
test_sender_create_destroy();
test_receiver_create_destroy();
test_sender_queue_capacities();
test_sender_zero_capacity();
test_receiver_fd_zero();
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_MULTIPROCESSING_H
#define TEST_MULTIPROCESSING_H
void test_multiprocessing();
#endif
+19 -2
View File
@@ -129,8 +129,7 @@ static void test_send_receive_status() {
io_set_bwlimit(0); io_set_bwlimit(0);
Status statuses[] = {STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT, Status statuses[] = {STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT,
STATUS_CHUNK, STATUS_CHECK, STATUS_DELTA_SIGNATURE, STATUS_DELTA_DATA, STATUS_CHUNK, STATUS_CHECK, STATUS_DELTA_SIGNATURE, STATUS_DELTA_DATA};
STATUS_KEEPALIVE, STATUS_ABORT, STATUS_CHECK_BATCH};
int count = sizeof(statuses) / sizeof(statuses[0]); int count = sizeof(statuses) / sizeof(statuses[0]);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
@@ -170,6 +169,23 @@ static void test_receive_str_truncated() {
close(p[0]); close(p[0]);
} }
static void test_receive_str_oversized() {
int p[2];
EXPECT_EQ_INT(pipe(p), 0);
io_set_fds(p[0], p[1]);
io_set_bwlimit(0);
/* Send a size exceeding MAX_STRING_SIZE */
size_t huge = MAX_STRING_SIZE + 1;
EXPECT_TRUE(send_n_data(0, &huge, sizeof(size_t)));
char* received = receive_str(0);
EXPECT_NULL(received);
close(p[0]);
close(p[1]);
}
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();
@@ -180,4 +196,5 @@ void test_protocol() {
test_send_receive_status(); test_send_receive_status();
test_receive_n_data_truncated(); test_receive_n_data_truncated();
test_receive_str_truncated(); test_receive_str_truncated();
test_receive_str_oversized();
} }
+9 -281
View File
@@ -15,11 +15,11 @@ static void test_scanner_single_file() {
const char* file1 = "test_scan_dir_single/file1.txt"; const char* file1 = "test_scan_dir_single/file1.txt";
const char* content1 = "hello scanner"; const char* content1 = "hello scanner";
EXPECT_EQ_INT(mkdir(dir, 0755), 0); mkdir(dir, 0755);
create_test_file(file1, content1); create_test_file(file1, content1);
DirectoryScanner* scanner = DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
EXPECT_NOT_NULL(scanner); EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner); Chunk* chunk = directory_scanner_next(scanner);
@@ -43,12 +43,12 @@ static void test_scanner_multiple_files() {
const char* content1 = "alpha"; const char* content1 = "alpha";
const char* content2 = "beta"; const char* content2 = "beta";
EXPECT_EQ_INT(mkdir(dir, 0755), 0); mkdir(dir, 0755);
create_test_file(file1, content1); create_test_file(file1, content1);
create_test_file(file2, content2); create_test_file(file2, content2);
DirectoryScanner* scanner = DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
EXPECT_NOT_NULL(scanner); EXPECT_NOT_NULL(scanner);
const Chunk* chunk = directory_scanner_next(scanner); const Chunk* chunk = directory_scanner_next(scanner);
@@ -82,13 +82,13 @@ static void test_scanner_subdirectory() {
const char* sub_file = "test_scan_sub/sub/sub_file.txt"; const char* sub_file = "test_scan_sub/sub/sub_file.txt";
const char* content = "nested content"; const char* content = "nested content";
EXPECT_EQ_INT(mkdir(root, 0755), 0); mkdir(root, 0755);
EXPECT_EQ_INT(mkdir(sub, 0755), 0); mkdir(sub, 0755);
create_test_file(root_file, content); create_test_file(root_file, content);
create_test_file(sub_file, content); create_test_file(sub_file, content);
DirectoryScanner* scanner = DirectoryScanner* scanner =
directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, 0, 0); directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, 0);
EXPECT_NOT_NULL(scanner); EXPECT_NOT_NULL(scanner);
int total_files = 0; int total_files = 0;
@@ -109,10 +109,10 @@ static void test_scanner_subdirectory() {
static void test_scanner_empty_directory() { static void test_scanner_empty_directory() {
const char* dir = "test_scan_empty"; const char* dir = "test_scan_empty";
EXPECT_EQ_INT(mkdir(dir, 0755), 0); mkdir(dir, 0755);
DirectoryScanner* scanner = DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0); directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
EXPECT_NOT_NULL(scanner); EXPECT_NOT_NULL(scanner);
const Chunk* chunk = directory_scanner_next(scanner); const Chunk* chunk = directory_scanner_next(scanner);
@@ -122,281 +122,9 @@ static void test_scanner_empty_directory() {
rmdir(dir); rmdir(dir);
} }
/* --- Exclude/include pattern and size filter edge cases (Issue #56) --- */
static void test_scanner_exclude_pattern() {
const char* dir = "test_scan_excl";
const char* f_txt = "test_scan_excl/keep.txt";
const char* f_tmp = "test_scan_excl/remove.tmp";
const char* content = "data";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(f_txt, content);
create_test_file(f_tmp, content);
char* exclude[] = {"*.tmp"};
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, exclude, 1, NULL, 0, 0, 0, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 1);
EXPECT_EQ_STR(chunk->items[0]->path, f_txt);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(f_txt);
unlink(f_tmp);
rmdir(dir);
}
static void test_scanner_exclude_subdirectory() {
const char* root = "test_scan_excl_sub";
const char* sub = "test_scan_excl_sub/sub";
const char* root_txt = "test_scan_excl_sub/root.txt";
const char* sub_txt = "test_scan_excl_sub/sub/data.txt";
const char* sub_tmp = "test_scan_excl_sub/sub/temp.tmp";
const char* content = "data";
EXPECT_EQ_INT(mkdir(root, 0755), 0);
EXPECT_EQ_INT(mkdir(sub, 0755), 0);
create_test_file(root_txt, content);
create_test_file(sub_txt, content);
create_test_file(sub_tmp, content);
char* exclude[] = {"*.tmp"};
DirectoryScanner* scanner =
directory_scanner_create((char*)root, false, 0, exclude, 1, NULL, 0, 0, 0, 0);
EXPECT_NOT_NULL(scanner);
int total = 0;
Chunk* chunk;
while ((chunk = directory_scanner_next(scanner)) != NULL) {
total += chunk->element_count;
for (int i = 0; i < chunk->element_count; i++) {
size_t len = strlen(chunk->items[i]->path);
EXPECT_TRUE(len < 4 || strcmp(chunk->items[i]->path + len - 4, ".tmp") != 0);
}
chunk_destroy(chunk);
}
EXPECT_EQ_INT(total, 2);
directory_scanner_destroy(scanner);
unlink(root_txt);
unlink(sub_txt);
unlink(sub_tmp);
rmdir(sub);
rmdir(root);
}
static void test_scanner_include_and_exclude() {
const char* dir = "test_scan_inc_exc";
const char* f_txt = "test_scan_inc_exc/a.txt";
const char* f_log = "test_scan_inc_exc/b.log";
const char* f_bak = "test_scan_inc_exc/c.bak";
const char* content = "filter";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(f_txt, content);
create_test_file(f_log, content);
create_test_file(f_bak, content);
char* exclude[] = {"*.bak"};
char* include[] = {"*.txt", "*.log"};
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 2, 0, 0, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 2);
int found_txt = 0, found_log = 0;
for (int i = 0; i < chunk->element_count; i++) {
if (strstr(chunk->items[i]->path, "a.txt"))
found_txt = 1;
if (strstr(chunk->items[i]->path, "b.log"))
found_log = 1;
}
EXPECT_TRUE(found_txt);
EXPECT_TRUE(found_log);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(f_txt);
unlink(f_log);
unlink(f_bak);
rmdir(dir);
}
static void test_scanner_max_size() {
const char* dir = "test_scan_max";
const char* small = "test_scan_max/small.txt";
const char* large = "test_scan_max/large.txt";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(small, "tiny");
create_test_file(large, "this_content_is_longer_than_ten_chars");
/* max_size = 10 — only files <= 10 bytes */
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 10, 0, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 1);
EXPECT_EQ_STR(chunk->items[0]->path, small);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(small);
unlink(large);
rmdir(dir);
}
static void test_scanner_min_size() {
const char* dir = "test_scan_min";
const char* empty_f = "test_scan_min/empty.txt";
const char* data_f = "test_scan_min/data.txt";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(empty_f, "");
create_test_file(data_f, "some content here");
/* min_size = 1 — only files >= 1 byte */
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 1, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 1);
EXPECT_EQ_STR(chunk->items[0]->path, data_f);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(empty_f);
unlink(data_f);
rmdir(dir);
}
static void test_scanner_size_range() {
const char* dir = "test_scan_range";
const char* tiny = "test_scan_range/tiny.txt";
const char* medium = "test_scan_range/med.txt";
const char* huge = "test_scan_range/huge.txt";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(tiny, "ab");
create_test_file(medium, "hello world");
create_test_file(huge, "this is a much larger file for testing size filters");
/* Only files between 3 and 20 bytes */
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 20, 3, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 1);
EXPECT_EQ_STR(chunk->items[0]->path, medium);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(tiny);
unlink(medium);
unlink(huge);
rmdir(dir);
}
static void test_scanner_mixed_patterns() {
/* Combine exclude, include, and size filters together */
const char* dir = "test_scan_mixed";
const char* a_txt = "test_scan_mixed/a.txt"; /* size ~= 5 */
const char* b_bin = "test_scan_mixed/b.bin"; /* size ~= 13 */
const char* c_txt = "test_scan_mixed/c.txt"; /* size ~= 5 */
const char* d_bak = "test_scan_mixed/d.bak"; /* size ~= 42 */
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(a_txt, "aaaaa");
create_test_file(b_bin, "bbbbbbbbbbbbb");
create_test_file(c_txt, "ccccc");
create_test_file(d_bak, "dddddddddddddddddddddddddddddddddddddddddd");
/* Exclude *.bak, include *.txt, min_size=3, max_size=10 */
char* exclude[] = {"*.bak"};
char* include[] = {"*.txt"};
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 1, 10, 3, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
/* Both a.txt and c.txt meet the criteria: .txt extension, size 5 <= 10 and >= 3 */
EXPECT_EQ_INT(chunk->element_count, 2);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(a_txt);
unlink(b_bin);
unlink(c_txt);
unlink(d_bak);
rmdir(dir);
}
static void test_scanner_no_patterns() {
/* Explicit test with no exclude/include patterns and no size filters.
* This verifies that NULL/0 for all pattern parameters works correctly. */
const char* dir = "test_scan_none";
const char* f1 = "test_scan_none/f1.txt";
const char* f2 = "test_scan_none/f2.txt";
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
create_test_file(f1, "first");
create_test_file(f2, "second");
DirectoryScanner* scanner =
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0, 0);
EXPECT_NOT_NULL(scanner);
Chunk* chunk = directory_scanner_next(scanner);
EXPECT_NOT_NULL(chunk);
EXPECT_EQ_INT(chunk->element_count, 2);
chunk_destroy(chunk);
EXPECT_NULL(directory_scanner_next(scanner));
directory_scanner_destroy(scanner);
unlink(f1);
unlink(f2);
rmdir(dir);
}
void test_scanner() { void test_scanner() {
test_scanner_single_file(); test_scanner_single_file();
test_scanner_multiple_files(); test_scanner_multiple_files();
test_scanner_subdirectory(); test_scanner_subdirectory();
test_scanner_empty_directory(); test_scanner_empty_directory();
/* Issue #56: scanner pattern edge cases */
test_scanner_exclude_pattern();
test_scanner_exclude_subdirectory();
test_scanner_include_and_exclude();
test_scanner_max_size();
test_scanner_min_size();
test_scanner_size_range();
test_scanner_mixed_patterns();
test_scanner_no_patterns();
} }
-47
View File
@@ -1,47 +0,0 @@
#include "test_transport_ssh.h"
#include "test_utils.h"
#include "transport_ssh.h"
static void test_ssh_connect_invalid_dest_no_colon() {
/* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL);
EXPECT_NULL(client);
}
static void test_ssh_connect_invalid_dest_empty() {
/* cppcheck-suppress constVariablePointer */
Client* client = client_connect_ssh("", 22, NULL);
EXPECT_NULL(client);
}
/* Test client_connect_ssh with malformed destination (just a colon).
* parse_remote_dest succeeds, ssh is exec'd and fails, but the function
* creates a Client that must be cleaned up. */
static void test_ssh_connect_malformed() {
Client* client = client_connect_ssh(":", 22, NULL);
/* ssh binary exists, so exec succeeds; the function returns a Client.
* We just verify it doesn't crash and clean up properly. */
if (client != NULL) {
client_disconnect(client);
client_delete(client);
}
EXPECT_TRUE(true);
}
/* Test client_connect_ssh with valid format but unreachable host.
* The function launches ssh which will fail to connect, returns a Client. */
static void test_ssh_connect_unreachable() {
Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL);
if (client != NULL) {
client_disconnect(client);
client_delete(client);
}
EXPECT_TRUE(true);
}
void test_transport_ssh() {
test_ssh_connect_invalid_dest_no_colon();
test_ssh_connect_invalid_dest_empty();
test_ssh_connect_malformed();
test_ssh_connect_unreachable();
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_TRANSPORT_SSH_H
#define TEST_TRANSPORT_SSH_H
void test_transport_ssh();
#endif
-43
View File
@@ -1,43 +0,0 @@
#include "test_transport_tcp.h"
#include "test_utils.h"
#include "transport_tcp.h"
#include <unistd.h>
static void test_server_create_ephemeral() {
Server* s = server_create(0);
EXPECT_NOT_NULL(s);
EXPECT_TRUE(s->file_descriptor >= 0);
EXPECT_EQ_INT(s->address.sin_family, AF_INET);
server_delete(&s);
EXPECT_NULL(s);
}
static void test_server_delete_null() {
Server* s = NULL;
server_delete(&s);
EXPECT_NULL(s);
}
static void test_client_create() {
Client* c = client_create();
EXPECT_NOT_NULL(c);
EXPECT_TRUE(c->file_descriptor >= 0);
EXPECT_EQ_INT(c->address.sin_family, AF_INET);
EXPECT_EQ_INT(c->ssh_child_pid, -1);
EXPECT_NULL(c->ssl);
EXPECT_NULL(c->ssl_ctx);
client_disconnect(c);
client_delete(c);
}
static void test_client_delete_null() {
Client* c = NULL;
client_delete(c);
}
void test_transport_tcp() {
test_server_create_ephemeral();
test_server_delete_null();
test_client_create();
test_client_delete_null();
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_TRANSPORT_TCP_H
#define TEST_TRANSPORT_TCP_H
void test_transport_tcp();
#endif
-24
View File
@@ -1,24 +0,0 @@
#include "test_transport_tls.h"
#include "test_utils.h"
#include "transport_tcp.h"
#include "transport_tls.h"
static void test_tls_global_init() {
bool ok = tls_global_init();
EXPECT_TRUE(ok);
}
static void test_server_create_tls_without_certs() {
Server* s = server_create(0);
EXPECT_NOT_NULL(s);
bool ok = server_create_tls(s, NULL, NULL, NULL);
EXPECT_TRUE(ok);
EXPECT_NOT_NULL(s->ssl_ctx);
server_delete(&s);
EXPECT_NULL(s);
}
void test_transport_tls() {
test_tls_global_init();
test_server_create_tls_without_certs();
}
-6
View File
@@ -1,6 +0,0 @@
#ifndef TEST_TRANSPORT_TLS_H
#define TEST_TRANSPORT_TLS_H
void test_transport_tls();
#endif
-15
View File
@@ -2,24 +2,9 @@
#define TEST_UTILS_H #define TEST_UTILS_H
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdbool.h> #include <stdbool.h>
// Detect if running under valgrind by checking /proc/self/maps for vgpreload.
// This is used to skip fork-based tests that are incompatible with valgrind
// (the instrumented parent runs too slowly, causing pipe timeouts).
static inline bool is_running_under_valgrind(void) {
FILE* f = fopen("/proc/self/maps", "r");
if (!f)
return false;
char buf[4096];
size_t n = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
buf[n] = '\0';
return strstr(buf, "vgpreload") != NULL;
}
// Global test suite status // Global test suite status
extern int tests_run; extern int tests_run;
extern int tests_failed; extern int tests_failed;