Compare commits
217 Commits
add-readme
...
2dd40d6a5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 2dd40d6a5b | |||
| 12ca4b13c8 | |||
| 2fed5dddaa | |||
| 28d076fc28 | |||
| 4383caa3bb | |||
| df0f52ce39 | |||
| c2ddda26ad | |||
| ddfdb3825a | |||
| 4dbb2b4f8b | |||
| 504a3a4d4f | |||
| 6a5a61b59f | |||
| fd7e98cb25 | |||
| b71d132b17 | |||
| e7634f6581 | |||
| 00b6f2064b | |||
| 41e814ad97 | |||
| 9e1afd0764 | |||
| e2d8659d94 | |||
| 0d7cb7230d | |||
| 38be7c090a | |||
| 5fe89eb49f | |||
| 3f73012b36 | |||
| ed6242cfc5 | |||
| a27f13d657 | |||
| eb8651e3d5 | |||
| e9ab84b5ea | |||
| 7c6e69ad81 | |||
| 51a984871c | |||
| 66d994f01b | |||
| 44d0f99de9 | |||
| 8f77395b58 | |||
| 91071a4ed5 | |||
| afa5aeca37 | |||
| 6501b15574 | |||
| 5c686b0655 | |||
| b10da86f87 | |||
| c7f34eaf9d | |||
| bd17f50f9b | |||
| d3b0cb9357 | |||
| a4b35e136b | |||
| 02ceb6828b | |||
| 0f9e689e39 | |||
| a7bb6454a4 | |||
| 8a222bffa6 | |||
| 4982f9f47d | |||
| 8531f9e4c1 | |||
| 413f69de90 | |||
| d7b9d4dbbc | |||
| b9840b123d | |||
| 971a070b65 | |||
| dc7ada552b | |||
| 5dfc262cf7 | |||
| ba6d42ea2d | |||
| 256e1d7567 | |||
| f6eeb69d77 | |||
| f899085de4 | |||
| 5ede5c8bec | |||
| 23b1d6660c | |||
| 1078b47955 | |||
| 02266710fb | |||
| 12557c351c | |||
| d73d5c9d85 | |||
| 045965f74c | |||
| 784c57bd13 | |||
| 1fec43be0e | |||
| 8141158a3d | |||
| 598e4e7514 | |||
| 4ed514ad1b | |||
| e16db56580 | |||
| f0c7bb791a | |||
| 34b46a92db | |||
| 51f20d1c91 | |||
| b28bac9f41 | |||
| 43ba149ad5 | |||
| 293ba5191e | |||
| 0642a43c2c | |||
| fed77f6ce0 | |||
| 6de0998f69 | |||
| 43cf0bd81e | |||
| 907baf3379 | |||
| 8b6550f8c6 | |||
| 20ede4391c | |||
| 864eb1316d | |||
| eff50346fd | |||
| 25383f893b | |||
| 46c650e9ae | |||
| 7876721906 | |||
| 5ed12f2bf0 | |||
| 03d6ba0da5 | |||
| f1af88a25c | |||
| 1905369c9f | |||
| a3acf4b8b6 | |||
| 478bdd6fe6 | |||
| e408b08443 | |||
| 2c904bfac5 | |||
| e9554c1e69 | |||
| 8f2205dd30 | |||
| 70ab76fe90 | |||
| ed7f8ee56e | |||
| 8c7ebbfe32 | |||
| cc09bedc07 | |||
| 5fe40a5242 | |||
| 9130ee4b03 | |||
| 36da0d6adb | |||
| e7087d518b | |||
| 3ea11b5984 | |||
| 33f70f7de3 | |||
| 45dd62f568 | |||
| 4f8ae5bb70 | |||
| e2dac589a8 | |||
| 1b87131e82 | |||
| 3dcaf0b79d | |||
| ef6aa143d0 | |||
| 09a76e2a9a | |||
| c2436bef59 | |||
| a62894be05 | |||
| 93a5768bf0 | |||
| bbdf027be8 | |||
| efb98d8964 | |||
| 07207cd71d | |||
| ea551c4a1d | |||
| eff00fa021 | |||
| ab24724cb0 | |||
| 2ff51246c3 | |||
| 284bf8f8bc | |||
| f778d1903c | |||
| 04aad76481 | |||
| 52571bc960 | |||
| d1a23fc4df | |||
| 78cc735cff | |||
| ca29109664 | |||
| 270623b90e | |||
| 8f4ea62ae7 | |||
| 81800fa661 | |||
| 56d26f083c | |||
| d36768792a | |||
| 02619cca5c | |||
| b3f69208ce | |||
| 783400d7e9 | |||
| afca5e4f69 | |||
| e099dc4843 | |||
| c2bdf68429 | |||
| 1a12484469 | |||
| a96d48f0ed | |||
| 372c559ef2 | |||
| 3b92501dc7 | |||
| 1012c5c151 | |||
| 5b4395ada1 | |||
| c0dfa52f67 | |||
| fbd7ccf4dd | |||
| 29ad48e5d6 | |||
| e4ab2a414c | |||
| c2ada295ca | |||
| 24f9a2afb0 | |||
| 1a6879530e | |||
| 2ec8d4202e | |||
| b650911945 | |||
| c55d89c40f | |||
| 1d90d3230e | |||
| 12ae8c7f7a | |||
| 5fa0dc11e1 | |||
| 660834d453 | |||
| 1f75522d21 | |||
| d4fdc16c9e | |||
| 1444eb0f59 | |||
| 11b1765719 | |||
| 4a1e666365 | |||
| b99b733025 | |||
| a1189e51eb | |||
| 7d528b1b4d | |||
| 251d2a3407 | |||
| 266c1e37c7 | |||
| b680a8dff4 | |||
| 895927fbad | |||
| 6a609c1b07 | |||
| 1c26a7a712 | |||
| 0c2364bd62 | |||
| 5170155112 | |||
| f77e6bd16a | |||
| 7a1c491f7f | |||
| a90e510424 | |||
| 39faff1c4c | |||
| 89274c0fe2 | |||
| 433a6952e5 | |||
| a48b3e3a17 | |||
| d0c57ba2bb | |||
| 2fb7203c82 | |||
| 01045d7d14 | |||
| e7c23c135b | |||
| 1c5099b963 | |||
| 7cacce24e3 | |||
| 88a4d91c2e | |||
| f65cd02627 | |||
| b9d1a8c281 | |||
| 4867e04677 | |||
| 7f6fa968ee | |||
| 42870ec719 | |||
| 3146cd7cde | |||
| b8186d4b52 | |||
| 1d1f93f800 | |||
| c4aeed9f4d | |||
| e0f64e8d43 | |||
| 43c0a1395d | |||
| 38019e7e32 | |||
| f189adcbff | |||
| a242a6c031 | |||
| fe8fca47b4 | |||
| fefa968945 | |||
| 1486ac7088 | |||
| 7c02b16325 | |||
| d176507047 | |||
| ae4c8ff9f0 | |||
| f4f9adb007 | |||
| 95e3bded60 | |||
| e6c55356c8 | |||
| 95568ae34e | |||
| 12ca9e736b |
@@ -0,0 +1,10 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 2
|
||||
ColumnLimit: 100
|
||||
PointerAlignment: Left
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
SortIncludes: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
@@ -0,0 +1,116 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: clang-format check
|
||||
run: find src/ tests/ -name '*.c' -o -name '*.h' | xargs clang-format --dry-run --Werror
|
||||
|
||||
- name: cppcheck
|
||||
run: cppcheck --enable=warning,style,performance,portability --suppress=missingIncludeSystem --error-exitcode=1 --inline-suppr src/ tests/
|
||||
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Unit Tests
|
||||
run: ctest --test-dir build --output-on-failure -j$(nproc)
|
||||
|
||||
- name: Integration Tests
|
||||
run: python3 -m pytest tests/integration/ -v --tb=short
|
||||
|
||||
sanitizers:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
sanitizer: [address, undefined]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build-${{ matrix.sanitizer }} -j$(nproc)
|
||||
|
||||
- name: Unit Tests
|
||||
run: ctest --test-dir build-${{ matrix.sanitizer }} --output-on-failure
|
||||
|
||||
fuzz-build:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure (clang + fuzz)
|
||||
run: CC=clang CXX=clang++ cmake -B build-fuzz -S . -DENABLE_FUZZ=ON
|
||||
|
||||
- name: Build fuzz targets
|
||||
run: cmake --build build-fuzz -j$(nproc)
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -S . -DENABLE_COVERAGE=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Unit Tests
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Coverage Report
|
||||
run: |
|
||||
lcov --capture --directory build --output-file coverage.info --branch-coverage --ignore-errors negative
|
||||
lcov --remove coverage.info '/usr/*' '*/tests/*' '*/_deps/*' --output-file coverage.info --branch-coverage --ignore-errors unused,negative
|
||||
lcov --list coverage.info
|
||||
|
||||
valgrind:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Valgrind Memcheck
|
||||
run: valgrind --leak-check=full --show-leak-kinds=definite --error-exitcode=1 ./build/tests
|
||||
env:
|
||||
FASTSYNC_UNDER_VALGRIND: "1"
|
||||
@@ -1,2 +1,7 @@
|
||||
build
|
||||
data_copied
|
||||
test_data/
|
||||
__pycache__/
|
||||
build-asan
|
||||
coverage.info
|
||||
build-*/
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# LSAN suppressions for FastSync
|
||||
# Add suppression entries here for known pre-existing leaks that cannot be
|
||||
# fixed immediately. Remove entries as leaks are fixed.
|
||||
#
|
||||
# Example format:
|
||||
# leak:function_name
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Designs system architecture, module interactions, data flow, and makes high-level design decisions for FastSync.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a system architect for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Make high-level design decisions. Evaluate trade-offs, plan module interactions, design data flow, and ensure architectural coherence across the codebase.
|
||||
|
||||
> **Environment rule:** for CI, dependency installation must use the project's custom Docker image (repo-root `Dockerfile`, same as CI). For local development, use `nix-shell` (see `README.md`). See `AGENTS.md`.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Module Map
|
||||
```
|
||||
src/client/ Client-side: CLI parsing, scanning, sending
|
||||
client_cli.c Entry point, argument parsing, config setup
|
||||
client_send.c Transfer orchestration, pipeline management
|
||||
scanner.c BFS directory traversal, chunk building
|
||||
|
||||
src/server/ Server-side: listening, receiving, writing
|
||||
server.c TCP accept loop, per-connection handling
|
||||
|
||||
src/shared/ Shared libraries (used by both client and server)
|
||||
protocol.c/h Wire protocol: status codes, send/receive primitives
|
||||
compression.c/h zstd streaming compression/decompression
|
||||
chunk.c/h File grouping and batch serialization
|
||||
queue.c/h Thread-safe bounded queue (producer-consumer)
|
||||
config.c/h Runtime configuration, serialization, parsing
|
||||
data.c/h Generic buffer type (Data)
|
||||
metadata.c/h File metadata (mode, uid, gid, mtime)
|
||||
file.c/h File representation
|
||||
array_list.c/h Dynamic array
|
||||
transport_tcp.c/h TCP client/server with sendfile() zero-copy
|
||||
transport_ssh.c/h SSH transport with ControlMaster
|
||||
transport_tls.c/h TLS encryption via OpenSSL
|
||||
multiprocessing.c/h Fork-based concurrency
|
||||
log.c/h Logging utilities
|
||||
utils.c/h Shared utilities
|
||||
```
|
||||
|
||||
### Data Flow — Client Transfer Pipeline
|
||||
```
|
||||
CLI args → Config
|
||||
→ DirectoryScanner (BFS, exclude/include patterns)
|
||||
→ Queue[Scanner → Loader]
|
||||
→ ChunkBuilder (groups files into ~10MB chunks)
|
||||
→ Queue[Loader → Sender]
|
||||
→ [Optional: Compression (zstd streaming)]
|
||||
→ [Optional: Chunk Serialization]
|
||||
→ Network (TCP sendfile / SSH pipe)
|
||||
→ Protocol framing (status codes + data)
|
||||
```
|
||||
|
||||
### Data Flow — Server Receive
|
||||
```
|
||||
TCP accept / SSH stdio
|
||||
→ Config receive
|
||||
→ Per-connection handler (fork)
|
||||
→ [Optional: Decompression]
|
||||
→ [Optional: Chunk deserialization]
|
||||
→ File write / metadata restore
|
||||
→ [Optional: Delete processing via manifest]
|
||||
```
|
||||
|
||||
### Threading Model
|
||||
- Client uses producer-consumer with C11 threads (`thrd_t`)
|
||||
- Bounded queues with `mtx_t` + `cnd_t` for backpressure
|
||||
- Scanner → Loader → Sender pipeline
|
||||
- Server uses `fork()` per connection, optional thread pool
|
||||
|
||||
### Transport Abstraction
|
||||
- `io_set_fds(read_fd, write_fd)` — set active file descriptors
|
||||
- `io_set_ssl(SSL*)` — transparent TLS wrapping
|
||||
- `io_set_bwlimit(bytes_per_sec)` — token-bucket throttling
|
||||
- All protocol functions use the active IO layer transparently
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Performance first** — zero-copy where possible, streaming compression, multithreading
|
||||
2. **Simplicity** — status-code-driven protocol, no complex state machines
|
||||
3. **Composability** — features enabled via flags (-c, -m, -s, -f, -M)
|
||||
4. **Backward compatibility** — version field in config for negotiation
|
||||
5. **Unix philosophy** — do one thing well, compose via CLI flags
|
||||
|
||||
## When Making Design Decisions
|
||||
|
||||
### Evaluate
|
||||
1. **Performance impact** — Will this slow down the hot path?
|
||||
2. **Complexity cost** — Does this add state, protocol changes, or new failure modes?
|
||||
3. **Backward compatibility** — Can old clients/servers handle this?
|
||||
4. **Testability** — Can this be unit tested independently?
|
||||
5. **Composability** — Does this compose with existing flags/features?
|
||||
|
||||
### Output Format
|
||||
|
||||
When proposing architecture changes:
|
||||
1. **Problem** — what needs to be solved or improved
|
||||
2. **Current behavior** — how it works now
|
||||
3. **Proposed design** — new architecture with data flow diagrams
|
||||
4. **Trade-offs** — what's gained vs what's lost
|
||||
5. **Migration path** — how to get from current to proposed
|
||||
6. **Affected modules** — which files need changes
|
||||
7. **Testing strategy** — how to verify the change works
|
||||
|
||||
### Anti-patterns to Watch For
|
||||
- God functions (>200 lines, doing too many things)
|
||||
- Circular dependencies between modules
|
||||
- Leaking transport details into application logic
|
||||
- Hardcoded constants that should be configurable
|
||||
- Missing error propagation (silent failures)
|
||||
- Thread safety violations when adding new shared state
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
**Always wait for CI to finish after every push.** Never report a task as complete or move on until CI has passed on the PR branch.
|
||||
|
||||
After every push:
|
||||
1. Use `tea actions runs list` to get the latest run ID for the branch.
|
||||
2. Poll its status until it leaves the "running" state (use a loop with sleep + sufficient timeout, e.g., 600000ms).
|
||||
3. Once completed, inspect the logs with `tea actions runs log <ID>` for every job.
|
||||
4. If any job failed, fix the issue, push again, and repeat from step 1.
|
||||
5. Only report done when ALL CI jobs pass.
|
||||
|
||||
Do not wait for the user to tell you CI failed — check proactively. The user should never have to inform you of a CI failure you could have caught yourself.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
description: Reviews C code for memory safety, thread safety, null checks, buffer overflows, and style conventions specific to the FastSync codebase.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a C code reviewer for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Review C source files for correctness, safety, and style. You have deep knowledge of this codebase's patterns and conventions.
|
||||
|
||||
## Codebase Context
|
||||
|
||||
### Project Structure
|
||||
- `src/shared/` — shared libraries (protocol, compression, queue, config, data, metadata, transport, etc.)
|
||||
- `src/client/` — client CLI, file sending, scanner
|
||||
- `src/server/` — TCP server
|
||||
- `tests/` — unit tests with custom framework
|
||||
|
||||
### Key Data Types
|
||||
- `Data` — generic buffer (`void *data`, `size_t size`). Always use `data_create()` / `data_destroy()`.
|
||||
- `Queue` — thread-safe bounded queue with optional `item_destroyer` callback. Use `queue_create()` / `queue_destroy()`.
|
||||
- `Config` — runtime configuration struct. Use `config_create()` / `config_delete()`.
|
||||
- `Chunk` — collection of files for batch transfer.
|
||||
- `FileMetadata` — mode, uid, gid, mtime fields.
|
||||
- `Server` / `Client` — TCP transport structs.
|
||||
|
||||
### Threading
|
||||
- Uses C11 `<threads.h>` (`thrd_t`, `mtx_t`, `cnd_t`), NOT pthreads directly.
|
||||
- Producer-consumer pattern with `queue_enqueue_multithreaded()` / `queue_dequeue_multithreaded()`.
|
||||
- Bounded queues use condition variables for signaling.
|
||||
|
||||
### Memory Conventions
|
||||
- All heap allocations use `malloc`/`calloc`/`realloc` + `free`.
|
||||
- Destroy functions (`data_destroy`, `queue_destroy`, `config_delete`, etc.) handle cleanup.
|
||||
- Ownership is transferred at function boundaries — document who owns what.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Memory Safety
|
||||
- Every `malloc`/`calloc` has a corresponding `free` on all code paths (including error paths).
|
||||
- No use-after-free: check that pointers aren't used after their destroy function is called.
|
||||
- No double-free: ensure destroy functions aren't called twice on the same object.
|
||||
- Null checks after allocation before use.
|
||||
- Buffer sizes are correct — no off-by-one in string operations (`strlen` + 1 for null terminator).
|
||||
- `Data` objects created with `data_create()` and freed with `data_destroy()`.
|
||||
|
||||
### Thread Safety
|
||||
- Shared state accessed under proper mutex protection.
|
||||
- No race conditions on queue operations — using `_multithreaded` variants when threads are involved.
|
||||
- Condition variable signals happen under the lock.
|
||||
- No deadlock potential — consistent lock ordering.
|
||||
- `done` flags checked properly in consumer loops.
|
||||
|
||||
### Security
|
||||
- No `strcpy`/`strcat`/`sprintf` — use `snprintf` with bounds.
|
||||
- `malloc` size calculations don't overflow (`count * sizeof(...)` checked).
|
||||
- Path traversal prevention: no `..` in received filenames.
|
||||
- No fixed-size stack buffers for unbounded network input.
|
||||
- TLS error codes checked after `SSL_read`/`SSL_write`.
|
||||
- No hardcoded certificates, keys, or credentials.
|
||||
- Private key file permissions checked.
|
||||
- Received file permissions validated (no SUID/SGID injection).
|
||||
- Symlink attack prevention in destination directory.
|
||||
- Denial of service: bounded memory allocation, malformed messages handled gracefully.
|
||||
|
||||
### Protocol Safety
|
||||
- `send_n_data` / `receive_n_data` return values checked.
|
||||
- Status codes validated before use.
|
||||
- Config serialization/deserialization handles partial reads.
|
||||
|
||||
### Style
|
||||
- Header guards: `#ifndef FILENAME_H` / `#define FILENAME_H` / `#endif`
|
||||
- Function naming: `snake_case`, prefixed by module (`queue_create`, `data_compress`, `config_send`).
|
||||
- `static` for file-local functions.
|
||||
- Consistent pointer style: `Type *name` (space before asterisk).
|
||||
- Error handling: return `false`/`NULL` on failure, log when appropriate.
|
||||
|
||||
## Output Format
|
||||
|
||||
For each issue found, report:
|
||||
1. **File and line** — exact location
|
||||
2. **Severity** — critical / warning / style
|
||||
3. **Category** — memory / thread / protocol / security / style
|
||||
4. **Description** — what's wrong and how to fix it
|
||||
|
||||
If the code is clean, say so explicitly. Be concise — don't pad with fluff.
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
description: Manages the CMake build system for FastSync — adding targets, source files, dependencies, compiler flags, and sanitizer configurations.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a CMake expert for the FastSync project — a high-performance file synchronization system built with CMake 3.22+ and C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Manage the CMake build system: add new targets, configure dependencies, set compiler flags, and handle build configurations.
|
||||
|
||||
## Current Build Setup
|
||||
|
||||
### `CMakeLists.txt` (project root)
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
project(FastFileTransfer)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
add_compile_options(-Wall -g -O3)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(xxhash GIT_REPOSITORY https://github.com/Cyan4973/xxHash GIT_TAG v0.8.3 SOURCE_SUBDIR cmake_unofficial)
|
||||
FetchContent_MakeAvailable(xxhash)
|
||||
|
||||
# Sanitizer option
|
||||
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
|
||||
set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
|
||||
if(SANITIZER STREQUAL "address")
|
||||
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
|
||||
add_link_options(-fsanitize=address)
|
||||
elseif(SANITIZER STREQUAL "thread")
|
||||
add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g)
|
||||
add_link_options(-fsanitize=thread)
|
||||
elseif(NOT SANITIZER STREQUAL "none")
|
||||
message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, none")
|
||||
endif()
|
||||
|
||||
option(STRICT_WARNINGS "Enable strict warnings" OFF)
|
||||
if(STRICT_WARNINGS)
|
||||
add_compile_options(-Wextra -Wpedantic -Werror)
|
||||
endif()
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
find_library(ZSTD_LIBRARY zstd)
|
||||
if(NOT ZSTD_LIBRARY)
|
||||
message(FATAL_ERROR "zstd library not found. Ensure it is in your nix-shell!")
|
||||
endif()
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
file(GLOB SHARED_SRCS "src/shared/*.c")
|
||||
file(GLOB SERVER_SRCS "src/server/*.c")
|
||||
file(GLOB CLIENT_SRCS "src/client/*.c")
|
||||
file(GLOB TEST_SRCS "tests/*.c")
|
||||
|
||||
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS})
|
||||
target_include_directories(server PRIVATE src/shared src/server src/client)
|
||||
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
|
||||
add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS})
|
||||
target_include_directories(client PRIVATE src/shared src/server src/client)
|
||||
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
|
||||
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c)
|
||||
target_include_directories(tests PRIVATE tests src/shared src/server src/client)
|
||||
target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
```
|
||||
|
||||
### Source Layout
|
||||
```
|
||||
src/shared/ — shared libraries (globbed as SHARED_SRCS)
|
||||
src/client/ — client sources (globbed as CLIENT_SRCS)
|
||||
src/server/ — server sources (globbed as SERVER_SRCS)
|
||||
tests/ — unit test sources (globbed as TEST_SRCS)
|
||||
tests/integration/ — Python pytest integration tests
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
- **zstd** — found via `find_library(ZSTD_LIBRARY zstd)`
|
||||
- **OpenSSL** — found via `find_package(OpenSSL REQUIRED)` (TLS 1.2+ transport)
|
||||
- **xxHash** — fetched via `FetchContent` from GitHub (delta transfer hashing, v0.8.3)
|
||||
- **pthreads** — found via `find_package(Threads REQUIRED)`
|
||||
- **C11 standard** — required
|
||||
- **CMake 3.22+** — minimum version
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use `file(GLOB ...)` for source collection (existing pattern).
|
||||
- All targets link `Threads::Threads`, `${ZSTD_LIBRARY}`, `OpenSSL::SSL`, `OpenSSL::Crypto`, and `xxhash`.
|
||||
- Include directories: `src/shared`, `src/server`, `src/client`, `tests` (for test target).
|
||||
- Sanitizer support: pass `-DSANITIZER=address` or `-DSANITIZER=thread` to cmake (live option in CMakeLists.txt).
|
||||
- Build with `cmake -B build -S . && cmake --build build -j$(nproc)`.
|
||||
- For CI, dependencies are provided by the project's custom Docker image (repo-root `Dockerfile`, same image CI uses). For local development, use `nix-shell`. Never add `apt-get install` / `pip install` to CI workflows. See `AGENTS.md`.
|
||||
|
||||
## When Making Changes
|
||||
|
||||
1. Preserve existing structure and conventions.
|
||||
2. Use `file(GLOB)` for new source directories (match existing pattern).
|
||||
3. Add new dependencies with `find_package` or `find_library`.
|
||||
4. When adding a new executable target, follow the pattern of existing targets.
|
||||
5. When adding a new library (static/shared), use `add_library` and follow the project's naming.
|
||||
6. For sanitizer builds, pass `-DSANITIZER=address` or `-DSANITIZER=thread` to cmake (matching CI's matrix strategy).
|
||||
7. Always verify the build compiles after changes.
|
||||
|
||||
## Sanitizer Configurations
|
||||
|
||||
Use the project's built-in `-DSANITIZER=` option (matching the CI matrix):
|
||||
```bash
|
||||
cmake -B build -S . -DSANITIZER=address # AddressSanitizer (memory errors)
|
||||
cmake --build build -j$(nproc)
|
||||
|
||||
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (race conditions)
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
For UndefinedBehaviorSanitizer (no `-DSANITIZER=undefined` option in CMakeLists.txt yet), use the manual flag approach:
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=undefined"
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
### Using ccache (faster rebuilds)
|
||||
```bash
|
||||
cmake -B build -S . -DCMAKE_C_COMPILER_LAUNCHER=ccache
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
### Cross-Compilation
|
||||
```bash
|
||||
# ARM cross-compile example
|
||||
cmake -B build-arm -S . \
|
||||
-DCMAKE_SYSTEM_NAME=Linux \
|
||||
-DCMAKE_SYSTEM_PROCESSOR=aarch64 \
|
||||
-DCMAKE_C_COMPILER=aarch64-linux-gnu-gcc
|
||||
```
|
||||
|
||||
### Release vs Debug Builds
|
||||
```bash
|
||||
# Release (optimized)
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Debug (with symbols, no optimization)
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
||||
|
||||
# RelWithDebInfo (optimized + debug symbols)
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
cmake -B build -S .
|
||||
cmake --build build -j$(nproc)
|
||||
./build/server
|
||||
./build/client
|
||||
./build/tests
|
||||
```
|
||||
|
||||
## Sanitizer Integration
|
||||
|
||||
The project uses a single `SANITIZER` cache variable in `CMakeLists.txt`:
|
||||
```cmake
|
||||
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
|
||||
set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
|
||||
```
|
||||
Supported values: `address`, `thread`, `none`. Unknown values trigger `FATAL_ERROR`.
|
||||
|
||||
Build with:
|
||||
```bash
|
||||
cmake -B build -S . -DSANITIZER=address
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
To add support for a new sanitizer (e.g., UBSan), add an `elseif(SANITIZER STREQUAL "undefined")` block following the existing `address`/`thread` pattern.
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,145 @@
|
||||
---
|
||||
description: Explains FastSync code, architecture, and design decisions to developers new to the codebase.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a code explainer for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Make the codebase understandable. Explain code sections, architecture decisions, data flow, and how components interact. Help developers new to the project get productive quickly.
|
||||
|
||||
## Project Quick-Start
|
||||
|
||||
### What FastSync Does
|
||||
FastSync is a file synchronization tool (like rsync, but faster). It transfers files from a source to a destination over TCP or SSH, with optional compression, multithreading, and metadata preservation.
|
||||
|
||||
### Key Concepts
|
||||
1. **Chunks** — files are grouped into chunks (~10MB) for batch transfer
|
||||
2. **Pipeline** — three stages: scan → load → send, connected by thread-safe queues
|
||||
3. **Protocol** — status-code-driven exchange over TCP/SSH
|
||||
4. **Transport** — pluggable: TCP (with optional TLS), SSH (via subprocess)
|
||||
5. **Incremental sync** — skip files unchanged since last transfer (size + mtime)
|
||||
|
||||
### Running the Project
|
||||
```bash
|
||||
# Build
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
|
||||
# Server (TCP mode)
|
||||
./build/server
|
||||
|
||||
# Client (TCP mode)
|
||||
./build/client --source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk
|
||||
|
||||
# Client (SSH mode, rsync-style)
|
||||
./build/client /path/to/send user@host:/path/to/receive
|
||||
|
||||
# Run tests
|
||||
./build/tests # unit tests
|
||||
python3 test.py # integration tests
|
||||
```
|
||||
|
||||
## Code Walkthrough
|
||||
|
||||
### Client Entry Point (`src/client/client_cli.c`)
|
||||
- Parses CLI arguments using `getopt_long`
|
||||
- Creates `Config` struct with all options
|
||||
- Detects SSH destinations (contains `:`)
|
||||
- Calls into `client_send.c` for the actual transfer
|
||||
|
||||
### Transfer Pipeline (`src/client/client_send.c`)
|
||||
The client transfer is a three-stage pipeline:
|
||||
|
||||
```
|
||||
Stage 1: Scanner (main thread)
|
||||
- BFS traversal of source directory
|
||||
- Builds chunks of files up to chunk_size
|
||||
- Pushes chunks into queue_1
|
||||
|
||||
Stage 2: Loader (worker threads)
|
||||
- Pops chunks from queue_1
|
||||
- Reads file contents into memory
|
||||
- Pushes loaded chunks into queue_2
|
||||
|
||||
Stage 3: Sender (main thread)
|
||||
- Pops loaded chunks from queue_2
|
||||
- Optionally compresses (zstd)
|
||||
- Optionally serializes chunk
|
||||
- Sends over TCP or SSH
|
||||
```
|
||||
|
||||
### Scanner (`src/client/scanner.c`)
|
||||
- Recursive BFS directory traversal
|
||||
- Respects `--exclude` and `--include` glob patterns
|
||||
- Groups files into chunks based on `chunk_size`
|
||||
- Handles `--max-size` and `--min-size` filtering
|
||||
|
||||
### Protocol (`src/shared/protocol.c`)
|
||||
Wire protocol for client-server communication:
|
||||
1. Client sends `Config` (serialized)
|
||||
2. For each file/chunk: status code + data
|
||||
3. If `--delete`: client sends manifest, server removes extras
|
||||
4. Client sends `STATUS_FINISHED`, server responds `STATUS_OK`
|
||||
|
||||
Status codes: `OK`, `ERROR`, `FINISHED`, `NEXT`, `CHUNK`, `MANIFEST`, `CHECK`
|
||||
|
||||
### Data Types
|
||||
|
||||
#### `Data` (`src/shared/data.h`)
|
||||
Generic buffer: `{ void *data; size_t size; }`. Always create with `data_create()` and free with `data_destroy()`.
|
||||
|
||||
#### `Queue` (`src/shared/queue.h`)
|
||||
Thread-safe bounded queue. Supports both single-threaded (`queue_enqueue`/`queue_dequeue`) and multi-threaded (`queue_enqueue_multithreaded`/`queue_dequeue_multithreaded`) access.
|
||||
|
||||
#### `Config` (`src/shared/config.h`)
|
||||
All runtime parameters. Serialized and sent over wire at transfer start. Fields include transport type, compression settings, chunk size, TLS config, exclude/include patterns.
|
||||
|
||||
#### `Chunk` (`src/shared/chunk.h`)
|
||||
Collection of files for batch transfer. Serialized with file count, then per-file: path, content, optional metadata.
|
||||
|
||||
### Server (`src/server/server.c`)
|
||||
- TCP mode: listens on port (default 8080), forks per connection
|
||||
- SSH mode: `--stdio` flag, runs once then exits
|
||||
- Receives config, processes files, handles `--delete` manifests
|
||||
|
||||
## Common Questions
|
||||
|
||||
### "How does compression work?"
|
||||
zstd streaming compression via `ZSTD_compressStream2`/`ZSTD_decompressStream`. Compression happens per-chunk in the sender stage. Level 1-22 (default 5). Streaming means memory usage stays bounded regardless of file size.
|
||||
|
||||
### "How does sendfile() work?"
|
||||
On Linux, `sendfile()` copies data directly from kernel file buffer to socket, bypassing userspace. ~2x faster for large files. Enabled with `-f` flag. Only works with TCP (not SSH, not compression).
|
||||
|
||||
### "How does incremental sync work?"
|
||||
Client sends file metadata (path, size, mtime) to server. Server checks if destination file has same size+mtime. If match, server responds `STATUS_OK` (skip). If mismatch, server responds `STATUS_NEXT` (send).
|
||||
|
||||
### "How does --delete work?"
|
||||
After all files are sent, client sends a manifest of all transferred paths. Server walks destination tree and removes any file/directory not in the manifest.
|
||||
|
||||
### "How does SSH transport work?"
|
||||
Client creates a `socketpair()`, `fork()`s, child `execvp("ssh", ...)` with the server binary. Uses SSH ControlMaster for connection reuse. Data flows through the socketpair.
|
||||
|
||||
### "How does TLS work?"
|
||||
OpenSSL TLS 1.2+ wraps the TCP connection. `SSL_read`/`SSL_write` transparently replace `read`/`write` via `io_set_ssl()`. Certificate verification optional with `--ca`.
|
||||
|
||||
## Explanation Guidelines
|
||||
|
||||
When explaining code:
|
||||
1. **Start with context** — what module, what it does in the bigger picture
|
||||
2. **Show the data flow** — what goes in, what comes out
|
||||
3. **Highlight non-obvious parts** — why this design, not that
|
||||
4. **Reference the source** — `file:line` for key functions
|
||||
5. **Connect to the protocol** — how this piece talks to other pieces
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,323 @@
|
||||
---
|
||||
description: Scans the FastSync codebase for code quality issues — god functions, duplication, cyclomatic complexity, error handling gaps, naming/style violations.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a code quality guardian for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Scan the codebase for code quality improvements. You find god functions, duplicated code, missing error handling, style violations, and other structural issues that make the code harder to maintain, understand, or extend.
|
||||
|
||||
> **Environment rule:** for CI, dependency installation must use the project's custom Docker image (repo-root `Dockerfile`, same as CI). For local development, use `nix-shell` (see `README.md`). See `AGENTS.md`.
|
||||
|
||||
## Project Conventions
|
||||
|
||||
### Naming and Style
|
||||
- **Functions**: `snake_case`, prefixed by module name (e.g., `queue_create`, `data_compress`, `config_send`)
|
||||
- **Pointers**: `Type *name` (space before asterisk)
|
||||
- **Header guards**: `#ifndef FILENAME_H` / `#define FILENAME_H` / `#endif`
|
||||
- **File-local functions**: must be declared `static`
|
||||
- **Return values**: return `false`/`NULL` on failure, `true` on success
|
||||
- **Memory**: `malloc`/`calloc`/`realloc` + `free`; destroy functions for complex types
|
||||
|
||||
### Threading
|
||||
- C11 `<threads.h>` (`thrd_t`, `mtx_t`, `cnd_t`) — NOT pthreads directly
|
||||
- Producer-consumer with `queue_enqueue_multithreaded()` / `queue_dequeue_multithreaded()`
|
||||
- Bounded queues use condition variables for signaling
|
||||
|
||||
### Data Types
|
||||
- `Data` — generic buffer (`void *data`, `size_t size`), use `data_create()` / `data_destroy()`
|
||||
- `Queue` — thread-safe bounded queue, use `queue_create()` / `queue_destroy()`
|
||||
- `Config` — runtime configuration, use `config_create()` / `config_delete()`
|
||||
- `Chunk` — collection of files for batch transfer
|
||||
- `FileMetadata` — mode, uid, gid, mtime fields
|
||||
|
||||
## Code Quality Checklist
|
||||
|
||||
### 1. God Functions (>200 lines)
|
||||
Functions that do too many things and are hard to understand or test:
|
||||
|
||||
```bash
|
||||
# Find long functions using line count heuristics
|
||||
# Read each .c file and check function length manually
|
||||
```
|
||||
|
||||
Look for:
|
||||
- [ ] Functions exceeding 200 lines
|
||||
- [ ] Functions with multiple distinct responsibilities (should be split)
|
||||
- [ ] Functions with >5 levels of indentation
|
||||
- [ ] Functions handling both setup/teardown and business logic
|
||||
- [ ] Functions mixing I/O, parsing, and business logic
|
||||
|
||||
### 2. Deeply Nested Conditionals (Cyclomatic Complexity)
|
||||
- [ ] If-else chains deeper than 4 levels
|
||||
```c
|
||||
if (a) {
|
||||
if (b) {
|
||||
if (c) {
|
||||
if (d) {
|
||||
// too deep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
- [ ] Switch statements with many cases that could be replaced by lookup tables
|
||||
- [ ] Complex ternary expressions nested inside other expressions
|
||||
- [ ] Loop inside conditional inside loop (deep nesting)
|
||||
- [ ] Functions with many `if-return` early exits that obscure flow
|
||||
|
||||
### 3. Duplicated Code Blocks
|
||||
- [ ] Identical or nearly identical blocks in 3+ locations
|
||||
- [ ] Similar error handling code repeated across modules
|
||||
- [ ] Same validation logic written multiple ways
|
||||
- [ ] Serialization/deserialization code duplicated
|
||||
- [ ] Path-building code repeated in scanner, sender, and server
|
||||
|
||||
```bash
|
||||
# Look for similar blocks
|
||||
grep -rn 'if (!send_n_data' src/ --include="*.c"
|
||||
grep -rn 'if (!receive_n_data' src/ --include="*.c"
|
||||
grep -rn 'snprintf.*path' src/ --include="*.c"
|
||||
```
|
||||
|
||||
### 4. Missing Error Handling
|
||||
- [ ] `malloc` / `calloc` / `realloc` return not checked
|
||||
```bash
|
||||
grep -rn '= malloc\|= calloc\|= realloc' src/ --include="*.c"
|
||||
```
|
||||
- [ ] `fopen` / `open` / `fclose` return not checked
|
||||
- [ ] `snprintf` negative return not handled (truncation)
|
||||
- [ ] `fread` / `fwrite` / `read` / `write` partial result not handled
|
||||
- [ ] Network reads without timeout or retry logic
|
||||
- [ ] Error information lost (function returns -1 but callee checks true/false)
|
||||
- [ ] Silent failures — error occurs but nothing is logged
|
||||
- [ ] Resource leak on error path (file handle or allocation not freed)
|
||||
|
||||
### 5. Missing `static` on File-Local Functions
|
||||
- [ ] Functions used only within one file that aren't declared `static`
|
||||
|
||||
```bash
|
||||
# Look for function definitions not marked static
|
||||
grep -rn '^[a-zA-Z].*(' src/ --include="*.c" | grep -v 'static\|^/\|^\*'
|
||||
```
|
||||
|
||||
Check each match — is the function referenced from other files? If not, it should be `static`.
|
||||
|
||||
### 6. Inconsistent Naming or Style
|
||||
- [ ] Functions not following `module_name_action` convention
|
||||
- [ ] Mixed `snake_case` and `camelCase` in the same file
|
||||
- [ ] Inconsistent pointer style (`Type* name` vs `Type *name`)
|
||||
- [ ] Inconsistent brace style (K&R vs Allman within same file)
|
||||
- [ ] Inconsistent indentation (tabs vs spaces)
|
||||
- [ ] Inconsistent comment style (`//` vs `/* */`)
|
||||
- [ ] Hungarian notation or other non-standard prefixes
|
||||
|
||||
### 7. Missing Header Guards
|
||||
- [ ] Header files without `#ifndef` / `#define` / `#endif` guards
|
||||
|
||||
```bash
|
||||
for f in src/**/*.h; do
|
||||
if ! grep -q '#ifndef\|#pragma once' "$f"; then
|
||||
echo "MISSING GUARD: $f"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### 8. Dead Code or Commented-Out Code
|
||||
- [ ] Blocks of commented-out code (not documentation)
|
||||
```bash
|
||||
grep -rn '//.*;' src/ --include="*.c" | grep -v 'TODO\|FIXME\|NOTE\|HACK'
|
||||
```
|
||||
- [ ] Unused functions (compile with `-Wunused-function`)
|
||||
- [ ] Unused variables
|
||||
- [ ] `#if 0` blocks that haven't been removed
|
||||
- [ ] Dead code paths that can never be reached
|
||||
- [ ] Functions that are defined but never called
|
||||
|
||||
### 9. Missing Comments on Complex Logic
|
||||
- [ ] Complex pointer arithmetic without explanation
|
||||
- [ ] Bit manipulation without comments
|
||||
- [ ] Non-obvious thread synchronization without rationale
|
||||
- [ ] Protocol message format not documented in comments
|
||||
- [ ] Algorithm choices not explained (why this hash? why this data structure?)
|
||||
- [ ] Error codes or magic numbers without symbolic names or comments
|
||||
|
||||
### 10. Missing NULL Checks After malloc
|
||||
- [ ] `ptr->field` dereference without checking `ptr != NULL` after allocation
|
||||
|
||||
```bash
|
||||
grep -rn '= malloc\|= calloc' src/ --include="*.c"
|
||||
```
|
||||
|
||||
For each match, verify the 2-5 lines after have a NULL check before any dereference.
|
||||
|
||||
### 11. Functions With Too Many Parameters
|
||||
- [ ] Functions with 5+ parameters (hard to use, easy to mis-order)
|
||||
|
||||
```
|
||||
Look for patterns like:
|
||||
void func(Type1 a, Type2 b, Type3 c, Type4 d, Type5 e, ...)
|
||||
```
|
||||
|
||||
Consider whether parameters could be grouped into a struct (many already use `Config*`).
|
||||
|
||||
### 12. Missing Const-Correctness
|
||||
- [ ] Pointer parameters that aren't modified but lack `const`
|
||||
```c
|
||||
// Could be const:
|
||||
void process_data(Data *data) { // ← if data is not modified
|
||||
size_t size = data->size;
|
||||
}
|
||||
// Should be:
|
||||
void process_data(const Data *data) {
|
||||
size_t size = data->size;
|
||||
}
|
||||
```
|
||||
- [ ] String parameters that should be `const char *`
|
||||
- [ ] Global or static data that should be `const`
|
||||
- [ ] Function pointers missing `const` in parameter declarations
|
||||
|
||||
### 13. Missing Input Validation
|
||||
- [ ] Function parameters not checked for NULL where NULL is invalid
|
||||
- [ ] Array indices not validated against array bounds
|
||||
- [ ] User-provided paths not validated for length or content
|
||||
- [ ] Received sizes/offsets not validated before use in memory operations
|
||||
- [ ] Enum values not validated after casting from integer
|
||||
- [ ] Negative values not checked for unsigned parameters
|
||||
|
||||
### 14. Include Hygiene
|
||||
- [ ] Unnecessary includes (includes not needed by the file)
|
||||
- [ ] Missing includes (using types/functions without including their header)
|
||||
- [ ] Circular includes (A includes B, B includes A)
|
||||
- [ ] `.c` files including other `.c` files
|
||||
- [ ] Inconsistent include style (`"header.h"` vs `<header.h>`)
|
||||
|
||||
### 15. Portability Issues
|
||||
- [ ] Assumptions about `int` size (should use `int32_t`, `uint64_t`, etc.)
|
||||
- [ ] Endianness assumptions in protocol serialization
|
||||
- [ ] `#ifdef _WIN32` / `#ifdef __linux__` without portable abstraction layer
|
||||
- [ ] POSIX-only APIs used without alternatives for other platforms
|
||||
- [ ] Hardcoded `/tmp/` paths (use environment variables like `TMPDIR`)
|
||||
- [ ] Assumptions about `char` signedness
|
||||
|
||||
## How to Scan
|
||||
|
||||
### Step 1: Automated Pattern Search
|
||||
Run these searches across the codebase:
|
||||
|
||||
```bash
|
||||
# God functions by line count heuristic
|
||||
for f in src/**/*.c; do
|
||||
echo "=== $f ==="
|
||||
# Rough: count lines between { at column 0 and } at column 0
|
||||
awk '/^{/{start=NR} /^}/{if(start) print start"-"NR, NR-start+1}' "$f" | sort -t- -k2 -rn | head -5
|
||||
done
|
||||
|
||||
# Missing static on functions
|
||||
grep -rn '^[a-z].*(.*)' src/ --include="*.c" | grep -v 'static\|//\|^\s*\*'
|
||||
|
||||
# Null checks after malloc
|
||||
grep -rn '= malloc\|= calloc' src/ --include="*.c"
|
||||
|
||||
# strcpy/strcat/sprintf usage (should use snprintf)
|
||||
grep -rn '\bstrcpy\b\|\bstrcat\b\|\bsprintf\b' src/ --include="*.c" --include="*.h"
|
||||
|
||||
# Commented out code
|
||||
grep -rn '^\s*//.*;$' src/ --include="*.c"
|
||||
|
||||
# Header guard check
|
||||
for f in src/**/*.h; do
|
||||
base=$(basename "$f" .h | tr '[:lower:]' '[:upper:]')
|
||||
if ! head -5 "$f" | grep -q "#ifndef ${base}_H"; then
|
||||
echo "Non-standard guard: $f"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Step 2: Manual Code Review
|
||||
Review these key files for quality issues:
|
||||
1. `src/client/client_send.c` — complex orchestration, check for god functions
|
||||
2. `src/client/scanner.c` — directory traversal, check for complexity
|
||||
3. `src/server/server.c` — connection handling, check for error handling
|
||||
4. `src/shared/protocol.c` — serialization, check for duplication
|
||||
5. `src/shared/config.c` — config parsing, check for validation
|
||||
6. `src/shared/chunk.c` — batching logic, check for bounds
|
||||
|
||||
### Step 3: Build Warnings Check
|
||||
```bash
|
||||
cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||
cmake --build build -j$(nproc) 2>&1 | grep -E 'warning:|error:'
|
||||
```
|
||||
|
||||
Any warnings indicate quality issues.
|
||||
|
||||
## Output Format
|
||||
|
||||
Return findings in this structured format, one per issue found:
|
||||
|
||||
```
|
||||
## Finding: <Short descriptive title>
|
||||
- **Severity**: critical/high/medium/low
|
||||
- **Category**: quality
|
||||
- **Location**: file:line range
|
||||
- **Description**: what the quality issue is, including:
|
||||
- Why it's a problem (maintainability, readability, safety)
|
||||
- The specific violation or pattern
|
||||
- **Suggestion**: how to fix it, including:
|
||||
- Concrete code change or refactoring approach
|
||||
- Alternative design if applicable
|
||||
- **Labels**: quality, comma-separated additional labels
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
## Finding: client_send.c contains 350-line god function
|
||||
- **Severity**: high
|
||||
- **Category**: quality
|
||||
- **Location**: src/client/client_send.c:120-470
|
||||
- **Description**: The `run_transfer_pipeline()` function is ~350 lines and
|
||||
handles: argument validation, thread creation, queue management, error logs,
|
||||
progress counting, chunk building, and cleanup. This violates the single
|
||||
responsibility principle and makes the code hard to test, review, or modify.
|
||||
- **Suggestion**: Extract distinct phases into separate functions:
|
||||
1. `validate_config()` — validate arguments
|
||||
2. `start_pipeline_threads()` — create scanner, loader, sender threads
|
||||
3. `monitor_progress()` — wait for completion with progress
|
||||
4. `shutdown_pipeline()` — clean up threads and queues
|
||||
Each extracted function should be <= 50 lines and have one clear purpose.
|
||||
- **Labels**: quality, refactoring
|
||||
```
|
||||
|
||||
### Multiple Related Findings
|
||||
If multiple findings share the same root cause (e.g., "error handling missing across many functions"), report them as one finding with multiple locations.
|
||||
|
||||
### Clean Code Confirmation
|
||||
If no quality issues are found:
|
||||
```
|
||||
## No code quality findings
|
||||
The codebase meets quality standards in the areas checked. No issues found at this time.
|
||||
```
|
||||
|
||||
## Severity Guidelines
|
||||
|
||||
| Severity | Definition | Example |
|
||||
|---|---|---|
|
||||
| **critical** | Bug-causing pattern, will lead to incorrect behavior | Missing error handling on critical path |
|
||||
| **high** | Significant maintainability concern | 350-line god function, large duplicated block |
|
||||
| **medium** | Standard code quality issue | Missing `static`, minor duplication |
|
||||
| **low** | Style preference, code golf | Naming inconsistency, minor formatting |
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,169 @@
|
||||
---
|
||||
description: Debugs crashes, memory errors, and logic bugs in FastSync using valgrind, ASan, gdb, and structured root cause analysis.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a debugger for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Diagnose crashes, memory errors, hangs, and logic bugs. You use structured debugging methodology: reproduce → isolate → diagnose → fix → verify.
|
||||
|
||||
## Debugging Toolkit
|
||||
|
||||
### Memory Errors
|
||||
```bash
|
||||
# AddressSanitizer (fast, recommended first)
|
||||
cmake -B build -S . -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/client # or ./build/server
|
||||
|
||||
# Valgrind (slower, more thorough)
|
||||
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst --save-to-disk
|
||||
|
||||
# Valgrind with race detection
|
||||
valgrind --tool=helgrind ./build/client ...
|
||||
|
||||
# Valgrind with DRD (alternative race detector)
|
||||
valgrind --tool=drd ./build/client ...
|
||||
```
|
||||
|
||||
### Thread Sanitizer
|
||||
```bash
|
||||
cmake -B build -S . -DCMAKE_C_FLAGS="-fsanitize=thread" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
### GDB
|
||||
```bash
|
||||
# Build with debug info
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build -j$(nproc)
|
||||
|
||||
# Run under gdb
|
||||
gdb --args ./build/client --source-dir /tmp/src --dest-dir /tmp/dst
|
||||
|
||||
# Useful gdb commands
|
||||
(gdb) run
|
||||
(gdb) bt # full backtrace on crash
|
||||
(gdb) bt full # backtrace with local variables
|
||||
(gdb) info threads # list all threads
|
||||
(gdb) thread apply all bt # backtrace of all threads
|
||||
(gdb) print variable_name # inspect variable
|
||||
(gdb) watch *ptr # watch for changes to pointer
|
||||
(gdb) info locals # all local variables
|
||||
```
|
||||
|
||||
### Strace / Ltrace
|
||||
```bash
|
||||
# Trace system calls
|
||||
strace -f -e trace=network,write,read ./build/client ...
|
||||
|
||||
# Trace library calls
|
||||
ltrace ./build/client ...
|
||||
```
|
||||
|
||||
### Performance Profiling
|
||||
```bash
|
||||
# perf record + report
|
||||
perf record -g ./build/client ...
|
||||
perf report
|
||||
|
||||
# perf stat (hardware counters)
|
||||
perf stat ./build/client ...
|
||||
|
||||
# gprof
|
||||
gcc -pg -o client ...
|
||||
./build/client
|
||||
gprof ./build/client gmon.out
|
||||
```
|
||||
|
||||
## Common Bug Patterns in This Codebase
|
||||
|
||||
### 1. Memory Leaks
|
||||
- `data_create()` without matching `data_destroy()`
|
||||
- `queue_create()` without `queue_destroy()`
|
||||
- `config_create()` without `config_delete()`
|
||||
- `malloc()` in error paths that return without `free()`
|
||||
- `receive_str()` return value not freed
|
||||
|
||||
### 2. Use-After-Free
|
||||
- Accessing `queue` after `queue_destroy()`
|
||||
- Using `Data*` after `data_destroy()`
|
||||
- Dereferencing freed config fields
|
||||
|
||||
### 3. Thread Safety
|
||||
- Queue operations without mutex when threads are active
|
||||
- Condition variable signals outside critical section
|
||||
- `done` flag not checked atomically in consumer loops
|
||||
- Shared `Config` fields modified during transfer
|
||||
|
||||
### 4. Protocol Errors
|
||||
- `send_n_data` / `receive_n_data` return value not checked
|
||||
- Status code received but not validated
|
||||
- Partial reads (short reads on sockets)
|
||||
- Config deserialization mismatch between client/server
|
||||
|
||||
### 5. Buffer Overflows
|
||||
- `strcpy` without bounds checking (use `snprintf`)
|
||||
- Off-by-one in string operations (`strlen + 1` for null terminator)
|
||||
- Fixed-size buffers for paths (`PATH_MAX` consideration)
|
||||
|
||||
### 6. Signal Handling
|
||||
- `SIGPIPE` on broken TCP connections
|
||||
- `SIGCHLD` from forked server children
|
||||
- Interrupted system calls (`EINTR`)
|
||||
|
||||
## Debugging Workflow
|
||||
|
||||
### Step 1: Reproduce
|
||||
- Get exact command line that triggers the bug
|
||||
- Determine if it's deterministic or intermittent
|
||||
- Note the environment (OS, compiler, libraries)
|
||||
|
||||
### Step 2: Isolate
|
||||
- Binary search the code: comment out half the pipeline
|
||||
- Add `fprintf(stderr, "DEBUG: reached %s:%d\n", __FILE__, __LINE__)` markers
|
||||
- Reduce test case to minimum reproducible example
|
||||
|
||||
### Step 3: Diagnose
|
||||
- Run with ASan/valgrind for memory errors
|
||||
- Run with TSan for thread issues
|
||||
- Get backtrace under gdb
|
||||
- Check return values of all syscalls
|
||||
|
||||
### Step 4: Fix
|
||||
- Apply minimal fix (don't refactor while debugging)
|
||||
- Verify fix doesn't break existing tests
|
||||
- Add regression test if possible
|
||||
|
||||
### Step 5: Verify
|
||||
- Run `./build/tests` (unit tests)
|
||||
- Run `python3 test.py` (integration tests)
|
||||
- Run under valgrind again to confirm clean
|
||||
- Test under ASan again
|
||||
|
||||
## Output Format
|
||||
|
||||
For each bug found:
|
||||
1. **Symptom** — what the user sees (crash, hang, wrong output)
|
||||
2. **Root cause** — exact file:line and what's happening
|
||||
3. **Reproduction** — exact command to trigger
|
||||
4. **Fix** — the minimal code change needed
|
||||
5. **Verification** — how to confirm the fix works
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
description: Generates and maintains API documentation, protocol specs, and usage examples from the FastSync C source code.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a documentation generator for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Generate accurate documentation from the actual source code. Maintain API references, protocol specifications, and usage examples.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Source Layout
|
||||
```
|
||||
src/shared/ — shared libraries (protocol, compression, queue, config, data, metadata, transport, etc.)
|
||||
src/client/ — client CLI, file sending, scanner
|
||||
src/server/ — TCP server
|
||||
tests/ — unit tests
|
||||
```
|
||||
|
||||
### Key Headers to Document
|
||||
|
||||
| Header | Purpose |
|
||||
|--------|---------|
|
||||
| `data.h` | Generic buffer type (`Data`) |
|
||||
| `queue.h` | Thread-safe bounded queue |
|
||||
| `chunk.h` | File chunking for batch transfer |
|
||||
| `compression.h` | zstd streaming compression |
|
||||
| `config.h` | Runtime configuration |
|
||||
| `protocol.h` | Wire protocol (status codes, send/receive) |
|
||||
| `metadata.h` | File metadata (mode, uid, gid, mtime) |
|
||||
| `transport_tcp.h` | TCP client/server |
|
||||
| `transport_ssh.h` | SSH transport with ControlMaster |
|
||||
| `scanner.h` | Directory traversal and file scanning |
|
||||
| `file.h` | File representation |
|
||||
| `array_list.h` | Dynamic array |
|
||||
| `log.h` | Logging utilities |
|
||||
| `utils.h` | Shared utilities |
|
||||
|
||||
### README
|
||||
The project README at `README.md` contains:
|
||||
- Technical overview
|
||||
- System architecture
|
||||
- Protocol details
|
||||
- Command-line arguments
|
||||
- Environment variables
|
||||
- Build instructions
|
||||
- Benchmark results
|
||||
|
||||
## Documentation Types
|
||||
|
||||
### 1. API Reference (from headers)
|
||||
For each public function:
|
||||
- Signature (from the header)
|
||||
- Brief description
|
||||
- Parameters and return value
|
||||
- Memory ownership rules
|
||||
- Thread safety guarantees
|
||||
|
||||
### 2. Protocol Specification
|
||||
- Wire format byte layouts
|
||||
- Status code semantics
|
||||
- Transfer flow diagrams
|
||||
- Metadata encoding
|
||||
|
||||
### 3. Architecture Docs
|
||||
- Data flow diagrams
|
||||
- Component interactions
|
||||
- Threading model
|
||||
|
||||
### 4. Usage Examples
|
||||
- Command-line examples for common use cases
|
||||
- Build instructions
|
||||
- Integration scenarios
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use `file:line` references when pointing to source locations
|
||||
- Document actual behavior, not intended behavior
|
||||
- Include error conditions and edge cases
|
||||
- Keep docs close to the code they describe
|
||||
- Use markdown formatting suitable for terminal rendering
|
||||
|
||||
## When Generating Documentation
|
||||
|
||||
1. Read the actual source files first — don't assume behavior
|
||||
2. Cross-reference headers with implementations
|
||||
3. Verify examples actually compile and work
|
||||
4. Update README when adding/changing features
|
||||
5. Keep protocol docs in sync with code changes
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,295 @@
|
||||
---
|
||||
description: Scans the FastSync codebase for feature opportunities — TODOs, configurable hardcoded values, missing flags, protocol gaps, and comparisons with rsync.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a feature scout for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Scan the codebase for patterns that suggest new feature opportunities. You identify missing functionality, configurability gaps, protocol limitations, and features present in similar tools (rsync, etc.) that FastSync could adopt.
|
||||
|
||||
> **Environment rule:** for CI, dependency installation must use the project's custom Docker image (repo-root `Dockerfile`, same as CI). For local development, use `nix-shell` (see `README.md`). See `AGENTS.md`.
|
||||
|
||||
## Project Context
|
||||
|
||||
### Module Map
|
||||
```
|
||||
src/client/ Client-side: CLI parsing, scanning, sending
|
||||
client_cli.c Entry point, argument parsing, config setup
|
||||
client_send.c Transfer orchestration, pipeline management
|
||||
scanner.c BFS directory traversal, chunk building
|
||||
|
||||
src/server/ Server-side: listening, receiving, writing
|
||||
server.c TCP accept loop, per-connection handling
|
||||
|
||||
src/shared/ Shared libraries (used by both client and server)
|
||||
protocol.c/h Wire protocol: status codes, send/receive primitives
|
||||
compression.c/h zstd streaming compression/decompression
|
||||
chunk.c/h File grouping and batch serialization
|
||||
queue.c/h Thread-safe bounded queue (producer-consumer)
|
||||
config.c/h Runtime configuration, serialization, parsing
|
||||
data.c/h Generic buffer type (Data)
|
||||
metadata.c/h File metadata (mode, uid, gid, mtime)
|
||||
file.c/h File representation
|
||||
array_list.c/h Dynamic array
|
||||
transport_tcp.c/h TCP client/server with sendfile() zero-copy
|
||||
transport_ssh.c/h SSH transport with ControlMaster
|
||||
transport_tls.c/h TLS encryption via OpenSSL
|
||||
multiprocessing.c/h Fork-based concurrency
|
||||
log.c/h Logging utilities
|
||||
utils.c/h Shared utilities
|
||||
```
|
||||
|
||||
### Existing CLI Flags (from client_cli.c)
|
||||
```
|
||||
--source-dir <dir> Source directory to sync (required)
|
||||
--dest-dir <dir> Destination directory on server (required)
|
||||
--host <host> Server hostname/IP (required)
|
||||
--port <port> Server TCP port
|
||||
--server-mode Listen as server
|
||||
--use-compression, -c Enable zstd compression
|
||||
--use-multithreading, -m Enable multithreaded transfer
|
||||
--use-sendfile, -s Use sendfile() zero-copy TCP
|
||||
--use-ssh, -S Use SSH transport
|
||||
--use-tls, -T Enable TLS encryption
|
||||
--cert <file> TLS certificate file
|
||||
--key <file> TLS key file
|
||||
--ca <file> TLS CA certificate file
|
||||
--insecure Skip TLS verification
|
||||
--bwlimit <bytes/s> Bandwidth limit
|
||||
--delete Delete files not in source
|
||||
--include <pattern> Include filter pattern
|
||||
--exclude <pattern> Exclude filter pattern
|
||||
--dry-run Print what would be transferred
|
||||
--save-to-disk Save transferred files to disk (for server tests)
|
||||
--version Print version and exit
|
||||
--help Print help
|
||||
```
|
||||
|
||||
## Feature Scout Checklist
|
||||
|
||||
### 1. TODO / FIXME / HARDCODED / HACK Comments
|
||||
Search for keywords that suggest missing functionality:
|
||||
- [ ] `TODO` — planned but unimplemented work
|
||||
- [ ] `FIXME` — known issues that need fixing
|
||||
- [ ] `HACK` — workarounds that should be properly implemented
|
||||
- [ ] `XXX` — something to revisit
|
||||
- [ ] `hardcoded` — values that should be configurable
|
||||
- [ ] `// @` — custom annotation patterns
|
||||
- [ ] `#warning` — compiler warnings for unimplemented features
|
||||
|
||||
```bash
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX\|hardcoded" src/ --include="*.c" --include="*.h"
|
||||
```
|
||||
|
||||
### 2. Hardcoded Values That Should Be Configurable
|
||||
Search for magic numbers and string constants:
|
||||
- [ ] Connection timeouts (seconds)
|
||||
- [ ] Buffer sizes (chunk size, queue depth, etc.)
|
||||
- [ ] Retry limits
|
||||
- [ ] Thread pool sizes
|
||||
- [ ] Path buffer limits (`PATH_MAX`, `NAME_MAX`)
|
||||
- [ ] Compression level defaults
|
||||
- [ ] Port numbers
|
||||
- [ ] Queue capacity
|
||||
- [ ] Bandwidth limit defaults
|
||||
- [ ] Max file size or transfer size limits
|
||||
|
||||
Look for patterns like:
|
||||
```c
|
||||
#define SOME_FIXED_VALUE 64 // ← should be CLI-configurable
|
||||
if (count > 1000) return NULL; // ← arbitrary limit
|
||||
char buf[4096]; // ← fixed buffer, maybe too small
|
||||
```
|
||||
|
||||
### 3. Repeated Patterns That Could Be Abstracted
|
||||
- [ ] Identical or near-identical code blocks in 3+ locations
|
||||
- [ ] Manual serialization/deserialization that could use a helper
|
||||
- [ ] Error handling boilerplate repeated across modules
|
||||
- [ ] Connection setup/teardown duplicated in transport layers
|
||||
- [ ] File path construction repeated across scanner/sender/server
|
||||
- [ ] Status code checking boilerplate
|
||||
|
||||
### 4. Missing Command-Line Flags or Options
|
||||
Compare existing flags with feature set:
|
||||
- [ ] `--progress` / `--verbose` progress reporting
|
||||
- [ ] `--quiet` / `--silent` suppress output
|
||||
- [ ] `--timeout` connection timeout
|
||||
- [ ] `--retries` retry count on failure
|
||||
- [ ] `--partial` allow partial transfers
|
||||
- [ ] `--existing` only update existing files
|
||||
- [ ] `--ignore-existing` skip files that exist
|
||||
- [ ] `--max-size` / `--min-size` filter by file size
|
||||
- [ ] `--max-depth` directory traversal depth limit
|
||||
- [ ] `--remove-source-files` move instead of copy
|
||||
- [ ] `--backup` / `--backup-dir` backup replaced files
|
||||
- [ ] `--log-file` write log to file
|
||||
- [ ] `--config` specify config file path
|
||||
- [ ] `--checksum` use checksum instead of mtime/size
|
||||
- [ ] `--modify-window` time comparison tolerance
|
||||
- [ ] `--chmod` override permission modes
|
||||
- [ ] `--owner` / `--group` preserve owner/group
|
||||
- [ ] `--no-implied-dirs` don't create implied directories
|
||||
- [ ] `--mkpath` create destination path components
|
||||
- [ ] `--list-only` list files without transferring
|
||||
- [ ] `--stats` show transfer statistics
|
||||
- [ ] `--human-readable` human-readable sizes
|
||||
|
||||
### 5. Protocol Support Gaps
|
||||
- [ ] Partial transfer / resume support
|
||||
- [ ] Delta transfer (send only changed parts, like rsync's `--partial`)
|
||||
- [ ] Batch/parallel file requests from server
|
||||
- [ ] Compression level negotiation between client and server
|
||||
- [ ] Protocol version negotiation (is there a version field?)
|
||||
- [ ] Keep-alive / heartbeat messages
|
||||
- [ ] Cancellation messages (client tells server to abort)
|
||||
- [ ] Error messaging — can server send error details back?
|
||||
- [ ] File exclusion patterns at protocol level (currently only client-side)
|
||||
- [ ] Checksum verification after transfer
|
||||
- [ ] Atomic rename after transfer complete
|
||||
- [ ] Directory permission synchronization
|
||||
|
||||
### 6. Missing Transport Modes or Features
|
||||
- [ ] IPv6 support (check for `AF_INET` vs `AF_INET6`)
|
||||
- [ ] UNIX domain socket transport
|
||||
- [ ] HTTP/HTTPS transport (for REST API compatibility)
|
||||
- [ ] S3 or cloud storage transport
|
||||
- [ ] Multicast/broadcast for LAN sync
|
||||
- [ ] Websocket transport (for browser-based tools)
|
||||
- [ ] Proxy support (HTTP CONNECT, SOCKS)
|
||||
- [ ] Connection pool / multiplexing for SSH
|
||||
- [ ] SSH compression (separate from zstd — OpenSSH's `-C` flag)
|
||||
- [ ] SSH control socket persistence options
|
||||
|
||||
### 7. Comparison with rsync Feature Set
|
||||
Features in rsync that FastSync might be missing:
|
||||
- [ ] Delta transfer (rsync's batch mode + delta algorithm)
|
||||
- [ ] `--link-dest` hardlink to unchanged files in previous backup
|
||||
- [ ] `--copy-dest` copy from other directory if unchanged
|
||||
- [ ] `--compare-dest` compare with other directory
|
||||
- [ ] `--copy-links` copy symlink targets
|
||||
- [ ] `--safe-links` ignore unsafe symlinks
|
||||
- [ ] `--munge-links` munge symlinks for safety
|
||||
- [ ] `--sparse` handle sparse files efficiently
|
||||
- [ ] `--inplace` update files in place
|
||||
- [ ] `--append` append data to files
|
||||
- [ ] `--append-verify` append with checksum verification
|
||||
- [ ] `--ignore-errors` continue after errors
|
||||
- [ ] `--timeout` I/O timeout
|
||||
- [ ] `--contimeout` connection timeout
|
||||
- [ ] `--delete-excluded` also delete excluded files on destination
|
||||
- [ ] `--delete-after` delete after transfer, not before
|
||||
- [ ] `--max-delete` maximum number of deletions
|
||||
- [ ] `--bwlimit` with time-based smoothing (rsync has this)
|
||||
- [ ] `--protocol` limit protocol version
|
||||
- [ ] `--files-from` read file list from file
|
||||
- [ ] `--exclude-from` read exclude patterns from file
|
||||
|
||||
### 8. Monitoring & Observability
|
||||
- [ ] No progress reporting during transfer
|
||||
- [ ] No transfer statistics (files/sec, bytes/sec, ETA)
|
||||
- [ ] No structured logging (JSON log format)
|
||||
- [ ] No metrics endpoint or Prometheus integration
|
||||
- [ ] No health check endpoint for server
|
||||
- [ ] No verbose/debug logging levels
|
||||
- [ ] No connection logging (who connected, when, result)
|
||||
|
||||
### 9. Testing Gaps
|
||||
- [ ] No stress tests (large file counts, deep directories, etc.)
|
||||
- [ ] No network fault injection tests (packet loss, reorder, etc.)
|
||||
- [ ] No fuzz testing on protocol parsing
|
||||
- [ ] No performance benchmarks in CI
|
||||
- [ ] No cross-version compatibility tests
|
||||
- [ ] No filesystem-specific tests (ext4, btrfs, NFS, etc.)
|
||||
|
||||
## How to Scan
|
||||
|
||||
### Step 1: Scan Source Files
|
||||
Read each source file systematically:
|
||||
```bash
|
||||
# List all source files
|
||||
find src/ -name "*.c" -o -name "*.h" | sort
|
||||
|
||||
# Search for TODO/FIXME/HACK
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX" src/ --include="*.c" --include="*.h"
|
||||
|
||||
# Search for hardcoded constants
|
||||
g -rn "#define [A-Z_]*[0-9]" src/ --include="*.h"
|
||||
g -rn "int [a-z_]*limit\|int [a-z_]*timeout\|int [a-z_]*max" src/ --include="*.c"
|
||||
```
|
||||
|
||||
### Step 2: Review CLI and Config
|
||||
- Read `src/client/client_cli.c` for all supported flags
|
||||
- Read `src/shared/config.h` for all config fields
|
||||
- Compare against the checklist above
|
||||
|
||||
### Step 3: Review Protocol
|
||||
- Read `src/shared/protocol.h` for all status codes and message types
|
||||
- Read `src/shared/protocol.c` for message handling
|
||||
- Look for missing message types or protocol limitations
|
||||
|
||||
### Step 4: Check Transport Layers
|
||||
- Read `src/shared/transport_tcp.c`, `transport_ssh.c`, `transport_tls.c`
|
||||
- Look for missing transport features
|
||||
|
||||
### Step 5: Check Tests
|
||||
- Read test files to see what's tested and what's not
|
||||
- Look for test gaps that indicate missing features
|
||||
|
||||
## Output Format
|
||||
|
||||
Return findings in this structured format, one per feature suggestion:
|
||||
|
||||
```
|
||||
## Finding: <Short descriptive title>
|
||||
- **Severity**: critical/high/medium/low
|
||||
- **Category**: feature
|
||||
- **Location**: file:line range (or "codebase-wide" if applicable)
|
||||
- **Description**: what feature is missing and why it matters
|
||||
- **Suggestion**: how to implement it, including:
|
||||
- CLI flag name (if applicable)
|
||||
- Config struct field (if applicable)
|
||||
- Protocol changes needed (if applicable)
|
||||
- Migration considerations
|
||||
- **Labels**: enhancement, comma-separated additional labels
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
## Finding: Add --progress flag for transfer progress reporting
|
||||
- **Severity**: medium
|
||||
- **Category**: feature
|
||||
- **Location**: src/client/client_cli.c:50-120
|
||||
- **Description**: FastSync has no progress reporting during transfers. Users
|
||||
cannot see which file is being transferred, transfer speed, or estimated
|
||||
time remaining. This is a standard feature in rsync and most sync tools.
|
||||
- **Suggestion**: Add a `--progress` / `-P` flag. Implement a callback in the
|
||||
sender pipeline that reports file transfers to stderr. Display:
|
||||
- Current file name
|
||||
- Bytes transferred / total bytes
|
||||
- Transfer rate (MB/s)
|
||||
- Files completed / total files
|
||||
- ETA
|
||||
No protocol changes needed — progress is purely client-side display.
|
||||
- **Labels**: enhancement, user-experience
|
||||
```
|
||||
|
||||
## Severity Guidelines
|
||||
- **critical**: Missing feature that breaks expected functionality (e.g., no delete support)
|
||||
- **high**: Important feature that limits use cases (e.g., no SSH support)
|
||||
- **medium**: Nice-to-have that improves usability (e.g., progress reporting)
|
||||
- **low**: Minor polish or edge case (e.g., colorized output)
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
description: Designs and verifies integration tests, end-to-end workflows, and CI/CD pipeline configurations for FastSync.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are an integration specialist for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Design integration tests that verify the full transfer pipeline works end-to-end. Bridge the gap between unit tests (component-level) and production use (full system).
|
||||
|
||||
## Test Layers
|
||||
|
||||
### 1. Unit Tests (existing — `tests/`)
|
||||
- Component-level: queue, data, compression, config, chunk, scanner, protocol
|
||||
- Custom framework in `tests/test_utils.h`
|
||||
- Run: `./build/tests`
|
||||
|
||||
### 2. Integration Tests (existing — `tests/integration/`)
|
||||
- Full transfer pipeline: client → server → verify
|
||||
- Multiple configurations (TCP, SSH, TLS, compression, multithreading)
|
||||
- Network shaping (LAN, WAN profiles)
|
||||
- Feature tests (dry run, archive, exclude, delete, incremental, bandwidth limit)
|
||||
- Run: `python3 -m pytest tests/ -v --tb=short`
|
||||
|
||||
### 3. New: Focused Integration Tests
|
||||
When adding new features or fixing bugs, write targeted integration tests.
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
### Pattern 1: Transfer Round-Trip
|
||||
```bash
|
||||
# Setup
|
||||
mkdir -p /tmp/fastsync_test/src
|
||||
echo "test content" > /tmp/fastsync_test/src/file.txt
|
||||
|
||||
# Start server
|
||||
./build/server &
|
||||
SERVER_PID=$!
|
||||
sleep 0.5
|
||||
|
||||
# Run client
|
||||
./build/client --source-dir /tmp/fastsync_test/src \
|
||||
--dest-dir /tmp/fastsync_test/dst \
|
||||
--save-to-disk
|
||||
|
||||
# Verify
|
||||
diff /tmp/fastsync_test/src/file.txt /tmp/fastsync_test/dst/tmp/fastsync_test/src/file.txt
|
||||
|
||||
# Cleanup
|
||||
kill $SERVER_PID
|
||||
rm -rf /tmp/fastsync_test
|
||||
```
|
||||
|
||||
### Pattern 2: SSH Transfer
|
||||
```bash
|
||||
# Prerequisites: fastsync-server in PATH on localhost
|
||||
./build/client /tmp/fastsync_test/src localhost:/tmp/fastsync_test/dst \
|
||||
--save-to-disk
|
||||
```
|
||||
|
||||
### Pattern 3: TLS Transfer
|
||||
```bash
|
||||
# Generate test certs (if not already available)
|
||||
openssl req -x509 -newkey rsa:2048 -keyout /tmp/key.pem -out /tmp/cert.pem \
|
||||
-days 1 -nodes -subj '/CN=localhost'
|
||||
|
||||
# Server with TLS
|
||||
./build/server --tls --cert /tmp/cert.pem --key /tmp/key.pem &
|
||||
|
||||
# Client with TLS
|
||||
./build/client --tls --cert /tmp/cert.pem --key /tmp/key.pem \
|
||||
--source-dir /tmp/src --dest-dir /tmp/dst --save-to-disk
|
||||
```
|
||||
|
||||
### Pattern 4: Incremental Sync
|
||||
```bash
|
||||
# First sync
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst --save-to-disk -M
|
||||
|
||||
# Modify source
|
||||
echo "updated" >> /tmp/src/file.txt
|
||||
|
||||
# Second sync — should only transfer changed files
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst \
|
||||
--save-to-disk --incremental
|
||||
```
|
||||
|
||||
### Pattern 5: Delete Verification
|
||||
```bash
|
||||
# Initial sync
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst --save-to-disk -M
|
||||
|
||||
# Add extra file to dest
|
||||
echo "extra" > /tmp/dst/.../extra.txt
|
||||
|
||||
# Sync with --delete
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst \
|
||||
--save-to-disk --delete -M
|
||||
|
||||
# Verify extra.txt is gone
|
||||
test ! -f /tmp/dst/.../extra.txt
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### Gitea Workflow Structure (`.gitea/workflows/ci.yaml`)
|
||||
The project uses Gitea Actions. Key jobs:
|
||||
1. **build-and-test** — compile, unit tests, integration tests on push/PR
|
||||
2. **sanitizer** — ASan + UBSan build and test (separate job)
|
||||
3. **clang-tidy** — static analysis on C source files
|
||||
|
||||
### Adding a New CI Job
|
||||
```yaml
|
||||
jobs:
|
||||
new-job:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Configure
|
||||
run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
|
||||
- name: Build
|
||||
run: cmake --build build-${{ matrix.sanitizer }} -j$(nproc)
|
||||
- name: Symlink for integration tests
|
||||
run: ln -sf build-${{ matrix.sanitizer }} build
|
||||
- name: Unit Tests
|
||||
run: ./build-${{ matrix.sanitizer }}/tests
|
||||
- name: Integration Tests
|
||||
run: LSAN_OPTIONS=suppressions=.lsan-suppressions.txt python3 -m pytest tests/ -v --tb=short
|
||||
```
|
||||
The symlink step is required because `tests/conftest.py` expects `./build` to exist.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After any code change:
|
||||
- [ ] Unit tests pass: `./build/tests`
|
||||
- [ ] Integration tests pass: `python3 -m pytest tests/ -v --tb=short`
|
||||
- [ ] Build clean: no warnings with `-Wall`
|
||||
- [ ] No memory errors: ASan clean
|
||||
- [ ] No thread errors: TSan clean (if threading involved)
|
||||
|
||||
## Output Format
|
||||
|
||||
When designing integration tests:
|
||||
1. **Test scenario** — what's being tested
|
||||
2. **Setup** — prerequisites and test data
|
||||
3. **Commands** — exact commands to run
|
||||
4. **Verification** — how to check success
|
||||
5. **Cleanup** — how to remove test artifacts
|
||||
6. **CI integration** — how to add to the workflow
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,266 @@
|
||||
---
|
||||
description: Top-level orchestrator that analyzes the FastSync codebase by delegating to specialized sub-agents and creates GitHub issues from their findings.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are the issue creator for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
You are the primary orchestrator agent. Your job is to:
|
||||
1. Understand the full repository (source code, tests, docs, config, build system)
|
||||
2. Decide which specialized sub-agents to dispatch for analysis
|
||||
3. Delegate analysis work using the task tool
|
||||
4. Receive structured findings from sub-agents
|
||||
5. Create GitHub issues from those findings using `gh issue create`
|
||||
6. Coordinate the overall analysis workflow end-to-end
|
||||
|
||||
> **Environment rule:** for CI, dependency installation must use the project's custom Docker image (repo-root `Dockerfile`, same as CI). For local development, use `nix-shell` (see `README.md`). See `AGENTS.md`.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Module Map
|
||||
```
|
||||
src/client/ Client-side: CLI parsing, scanning, sending
|
||||
client_cli.c Entry point, argument parsing, config setup
|
||||
client_send.c Transfer orchestration, pipeline management
|
||||
scanner.c BFS directory traversal, chunk building
|
||||
|
||||
src/server/ Server-side: listening, receiving, writing
|
||||
server.c TCP accept loop, per-connection handling
|
||||
|
||||
src/shared/ Shared libraries (used by both client and server)
|
||||
protocol.c/h Wire protocol: status codes, send/receive primitives
|
||||
compression.c/h zstd streaming compression/decompression
|
||||
chunk.c/h File grouping and batch serialization
|
||||
queue.c/h Thread-safe bounded queue (producer-consumer)
|
||||
config.c/h Runtime configuration, serialization, parsing
|
||||
data.c/h Generic buffer type (Data)
|
||||
metadata.c/h File metadata (mode, uid, gid, mtime)
|
||||
file.c/h File representation
|
||||
array_list.c/h Dynamic array
|
||||
transport_tcp.c/h TCP client/server with sendfile() zero-copy
|
||||
transport_ssh.c/h SSH transport with ControlMaster
|
||||
transport_tls.c/h TLS encryption via OpenSSL
|
||||
multiprocessing.c/h Fork-based concurrency
|
||||
log.c/h Logging utilities
|
||||
utils.c/h Shared utilities
|
||||
```
|
||||
|
||||
### Data Flow — Client Transfer Pipeline
|
||||
```
|
||||
CLI args → Config
|
||||
→ DirectoryScanner (BFS, exclude/include patterns)
|
||||
→ Queue[Scanner → Loader]
|
||||
→ ChunkBuilder (groups files into ~10MB chunks)
|
||||
→ Queue[Loader → Sender]
|
||||
→ [Optional: Compression (zstd streaming)]
|
||||
→ [Optional: Chunk Serialization]
|
||||
→ Network (TCP sendfile / SSH pipe)
|
||||
→ Protocol framing (status codes + data)
|
||||
```
|
||||
|
||||
### Data Flow — Server Receive
|
||||
```
|
||||
TCP accept / SSH stdio
|
||||
→ Config receive
|
||||
→ Per-connection handler (fork)
|
||||
→ [Optional: Decompression]
|
||||
→ [Optional: Chunk deserialization]
|
||||
→ File write / metadata restore
|
||||
→ [Optional: Delete processing via manifest]
|
||||
```
|
||||
|
||||
### Threading Model
|
||||
- Client uses producer-consumer with C11 threads (`thrd_t`)
|
||||
- Bounded queues with `mtx_t` + `cnd_t` for backpressure
|
||||
- Scanner → Loader → Sender pipeline
|
||||
- Server uses `fork()` per connection, optional thread pool
|
||||
|
||||
### Transport Abstraction
|
||||
- `io_set_fds(read_fd, write_fd)` — set active file descriptors
|
||||
- `io_set_ssl(SSL*)` — transparent TLS wrapping
|
||||
- `io_set_bwlimit(bytes_per_sec)` — token-bucket throttling
|
||||
- All protocol functions use the active IO layer transparently
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Repository Reconnaissance
|
||||
First, read the repository structure to understand what exists:
|
||||
1. Scan `src/` directory layout (client, server, shared modules)
|
||||
2. Scan `tests/` directory for test files
|
||||
3. Read `CMakeLists.txt` for build targets and options
|
||||
4. Read `AGENTS.md` and `.gitea/workflows/ci.yaml` for CI/dev conventions
|
||||
5. Read `.opencode/agents/*.md` to understand available sub-agents
|
||||
6. Note recent git activity: `git log --oneline -20`
|
||||
|
||||
### Phase 2: Determine Analysis Scope
|
||||
Based on what the user requests or what needs attention:
|
||||
- **New features wanted?** → Dispatch `feature-scout` sub-agent
|
||||
- **Security audit needed?** → Dispatch `security-screener` sub-agent
|
||||
- **Code quality review?** → Dispatch `code-quality-guardian` sub-agent
|
||||
- **All of the above?** → Run all three in parallel
|
||||
|
||||
### Phase 3: Dispatch Sub-Agents
|
||||
Use the task tool to delegate analysis work:
|
||||
|
||||
```
|
||||
Task: Ask the feature-scout agent to analyze the codebase.
|
||||
Context: <provide summary of what was found in Phase 1>
|
||||
```
|
||||
|
||||
```
|
||||
Task: Ask the security-screener agent to analyze the codebase.
|
||||
Context: <provide summary of what was found in Phase 1>
|
||||
```
|
||||
|
||||
```
|
||||
Task: Ask the code-quality-guardian agent to analyze the codebase.
|
||||
Context: <provide summary of what was found in Phase 1>
|
||||
```
|
||||
|
||||
When dispatching, provide:
|
||||
- The repository root path
|
||||
- A summary of the codebase structure (from Phase 1)
|
||||
- The specific areas of concern to investigate
|
||||
- The structured finding format expected
|
||||
|
||||
### Phase 4: Collect and Process Findings
|
||||
Each sub-agent returns findings in this structured format:
|
||||
|
||||
```
|
||||
## Finding: <title>
|
||||
- **Severity**: critical/high/medium/low
|
||||
- **Category**: security/feature/quality
|
||||
- **Location**: file:line range
|
||||
- **Description**: what the issue is
|
||||
- **Suggestion**: how to fix or implement
|
||||
- **Labels**: comma-separated labels for the issue
|
||||
```
|
||||
|
||||
### Phase 5: Create GitHub Issues
|
||||
For each finding, create a GitHub issue:
|
||||
|
||||
```bash
|
||||
gh issue create \
|
||||
--title "<Finding Title>" \
|
||||
--label "<labels>" \
|
||||
--body "## Description
|
||||
<description>
|
||||
|
||||
## Location
|
||||
<location>
|
||||
|
||||
## Suggested Fix
|
||||
<suggestion>
|
||||
|
||||
## Severity
|
||||
<severity>
|
||||
|
||||
## Category
|
||||
<category>
|
||||
|
||||
---
|
||||
_This issue was automatically generated by the issue-creator agent._"
|
||||
```
|
||||
|
||||
### Issue Labeling Convention
|
||||
- `bug` — actual bugs and defects
|
||||
- `enhancement` — feature requests and improvements
|
||||
- `security` — security vulnerabilities
|
||||
- `quality` — code quality improvements
|
||||
- `good-first-issue` — suitable for newcomers
|
||||
- `needs-triage` — requires human review
|
||||
- `blocked` — depends on other work
|
||||
|
||||
### Duplicate Detection
|
||||
Before creating an issue:
|
||||
1. Check existing open issues: `gh issue list --state open --label "<label>"`
|
||||
2. Search for similar titles using `gh issue list --search "<keywords>"`
|
||||
3. If a similar issue exists, add a comment instead of creating a duplicate:
|
||||
```bash
|
||||
gh issue comment <issue-number> --body "Additional finding from automated analysis: <details>"
|
||||
```
|
||||
|
||||
## Sub-Agent Reference
|
||||
|
||||
### Available Sub-Agents
|
||||
|
||||
| Agent | File | Purpose |
|
||||
|---|---|---|
|
||||
| feature-scout | `.opencode/agents/feature-scout.md` | Scans for feature opportunities |
|
||||
| security-screener | `.opencode/agents/security-screener.md` | Scans for security vulnerabilities |
|
||||
| code-quality-guardian | `.opencode/agents/code-quality-guardian.md` | Scans for code quality improvements |
|
||||
| architect | `.opencode/agents/architect.md` | Architecture reviews |
|
||||
| c-reviewer | `.opencode/agents/c-reviewer.md` | C code correctness reviews |
|
||||
| debugger | `.opencode/agents/debugger.md` | Bug diagnosis |
|
||||
| refactorer | `.opencode/agents/refactorer.md` | Code refactoring |
|
||||
| security-auditor | `.opencode/agents/security-auditor.md` | Security audits |
|
||||
| test-writer | `.opencode/agents/test-writer.md` | Test development |
|
||||
| perf-analyst | `.opencode/agents/perf-analyst.md` | Performance analysis |
|
||||
| protocol-designer | `.opencode/agents/protocol-designer.md` | Protocol design |
|
||||
| cmake-expert | `.opencode/agents/cmake-expert.md` | CMake build system |
|
||||
| code-explainer | `.opencode/agents/code-explainer.md` | Code explanation |
|
||||
| doc-generator | `.opencode/agents/doc-generator.md` | Documentation |
|
||||
| integrator | `.opencode/agents/integrator.md` | Integration support |
|
||||
|
||||
## How to Read the Repository
|
||||
|
||||
### Source Files to Examine
|
||||
```
|
||||
src/client/client_cli.c — CLI argument parsing
|
||||
src/client/client_send.c — Transfer orchestration
|
||||
src/client/scanner.c — BFS directory scanner
|
||||
src/server/server.c — TCP server, connection handling
|
||||
src/shared/protocol.c — Wire protocol implementation
|
||||
src/shared/compression.c — zstd compression
|
||||
src/shared/chunk.c — File chunking/batching
|
||||
src/shared/queue.c — Thread-safe queue
|
||||
src/shared/config.c — Runtime config
|
||||
src/shared/data.c — Buffer type
|
||||
src/shared/metadata.c — File metadata
|
||||
src/shared/file.c — File representation
|
||||
src/shared/array_list.c — Dynamic array
|
||||
src/shared/transport_tcp.c — TCP transport
|
||||
src/shared/transport_ssh.c — SSH transport
|
||||
src/shared/transport_tls.c — TLS transport
|
||||
src/shared/multiprocessing.c — Fork helpers
|
||||
src/shared/log.c — Logging
|
||||
src/shared/utils.c — Utilities
|
||||
```
|
||||
|
||||
### Test Files to Examine
|
||||
```
|
||||
tests/ — Unit tests
|
||||
tests/test_queue.c — Queue tests
|
||||
tests/test_protocol.c — Protocol tests
|
||||
tests/test_config.c — Config tests
|
||||
tests/test_compression.c — Compression tests
|
||||
tests/test_data.c — Data buffer tests
|
||||
tests/test_metadata.c — Metadata tests
|
||||
tests/test_file.c — File tests
|
||||
tests/test_transport_tcp.c — TCP transport tests
|
||||
tests/test_transport_tls.c — TLS transport tests
|
||||
tests/test_array_list.c — Array list tests
|
||||
tests/pytest/ — Python integration tests
|
||||
```
|
||||
|
||||
### Build & Config Files
|
||||
```
|
||||
CMakeLists.txt — Top-level CMake
|
||||
cmake/ — CMake modules
|
||||
Dockerfile — CI Docker image
|
||||
.opencode/ — opencode agent configs
|
||||
```
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
description: Analyzes performance bottlenecks in the FastSync transfer pipeline and suggests concrete optimizations for chunking, compression, threading, and network transport.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a performance analyst for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Analyze the transfer pipeline for performance bottlenecks and suggest concrete, actionable optimizations. You understand the full data flow from scanner to network.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Transfer Pipeline
|
||||
```
|
||||
DirectoryScanner → Queue(Scanner→Loader) → ChunkBuilder → Queue(Loader→Sender) → Network Send
|
||||
```
|
||||
|
||||
1. **Scanner** — BFS traversal, builds file list, groups into chunks
|
||||
2. **Loader** — reads file contents into memory
|
||||
3. **Sender** — compresses + serializes + sends over TCP/SSH
|
||||
|
||||
### Key Components
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Scanner | `src/client/scanner.c` | BFS directory traversal, exclude patterns, chunk building |
|
||||
| Chunk | `src/shared/chunk.c` | File grouping (~10MB default), serialization |
|
||||
| Compression | `src/shared/compression.c` | Streaming zstd (levels 1–22) |
|
||||
| Queue | `src/shared/queue.c` | Thread-safe bounded queue with condition variables |
|
||||
| Transport TCP | `src/shared/transport_tcp.c` | TCP with `sendfile()` zero-copy |
|
||||
| Transport SSH | `src/shared/transport_ssh.c` | SSH with ControlMaster, socketpair |
|
||||
| Protocol | `src/shared/protocol.c` | Status codes, data send/receive |
|
||||
| Config | `src/shared/config.c` | Runtime parameters |
|
||||
|
||||
### Performance-Critical Paths
|
||||
|
||||
1. **Chunk size** (`DEFAULT_CHUNK_SIZE = 10MB`) — balances memory vs. transfer efficiency
|
||||
2. **Compression level** (1–22) — trades CPU for bandwidth
|
||||
3. **`sendfile()` zero-copy** — bypasses userspace, ~2× faster on loopback
|
||||
4. **Multithreading** — producer-consumer with thread-safe queues
|
||||
5. **SSH socketpair buffer** — set to 1MB for pipe throughput
|
||||
6. **Streaming compression** — `ZSTD_compressStream2` / `ZSTD_decompressStream`
|
||||
|
||||
## Analysis Framework
|
||||
|
||||
### When Analyzing, Consider
|
||||
|
||||
1. **CPU-bound vs I/O-bound** — Is the bottleneck CPU (compression) or I/O (disk/network)?
|
||||
2. **Memory allocation** — Are there excessive malloc/free cycles in hot paths?
|
||||
3. **Lock contention** — Are mutexes held too long? Is the queue the bottleneck?
|
||||
4. **Syscall overhead** — Are there unnecessary read/write cycles?
|
||||
5. **Pipeline stalls** — Is any stage starved or blocked?
|
||||
6. **Data copying** — Are there unnecessary memcpy operations?
|
||||
7. **Algorithmic** — Is the chunking/scanning algorithm optimal?
|
||||
|
||||
### Benchmark Context
|
||||
|
||||
From README benchmarks (25MB mixed files, localhost):
|
||||
- Best config: `-m -c` (multithread + compression) → 0.20s, 11.2× faster than rsync
|
||||
- `sendfile()` bypasses userspace → ~2× faster on localhost
|
||||
- Compression reduces wire data enough that transfer becomes latency-bound on WAN
|
||||
|
||||
## Output Format
|
||||
|
||||
For each bottleneck found:
|
||||
1. **Location** — file:line
|
||||
2. **Impact** — high / medium / low
|
||||
3. **Type** — CPU / IO / memory / lock / algorithmic
|
||||
4. **Current behavior** — what's happening
|
||||
5. **Suggested optimization** — concrete code change or approach
|
||||
6. **Expected impact** — estimated speedup or resource savings
|
||||
|
||||
## Profiling Commands
|
||||
|
||||
### perf (Linux, recommended)
|
||||
```bash
|
||||
# Record call graph
|
||||
perf record -g ./build/client [args...]
|
||||
perf report
|
||||
|
||||
# Hardware counters (cache misses, branch mispredictions, etc.)
|
||||
perf stat ./build/client [args...]
|
||||
|
||||
# Specific events
|
||||
perf stat -e cache-misses,cache-references,instructions,cycles ./build/client [args...]
|
||||
|
||||
# Flame graph
|
||||
perf record -g -F 99 ./build/client [args...]
|
||||
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
|
||||
```
|
||||
|
||||
### valgrind (memory profiling)
|
||||
```bash
|
||||
# Callgrind (CPU profiling)
|
||||
valgrind --tool=callgrind ./build/client [args...]
|
||||
callgrind_annotate callgrind.out.*
|
||||
|
||||
# Cachegrind (cache simulation)
|
||||
valgrind --tool=cachegrind ./build/client [args...]
|
||||
cg_annotate cachegrind.out.*
|
||||
|
||||
# Massif (heap profiling)
|
||||
valgrind --tool=massif ./build/client [args...]
|
||||
ms_print massif.out.*
|
||||
```
|
||||
|
||||
### gprof
|
||||
```bash
|
||||
cmake -B build -S . -DCMAKE_C_FLAGS="-pg" -DCMAKE_EXE_LINKER_FLAGS="-pg"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/client [args...]
|
||||
gprof ./build/client gmon.out > analysis.txt
|
||||
```
|
||||
|
||||
### Time Measurement
|
||||
```bash
|
||||
# Quick timing
|
||||
time ./build/client [args...]
|
||||
|
||||
# High precision
|
||||
perf stat -e task-clock ./build/client [args...]
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
```
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
description: Designs and extends the FastSync wire protocol — status codes, metadata format, chunk serialization, config serialization, and ensures backward compatibility.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a protocol designer for the FastSync project — a high-performance file synchronization system with a custom binary wire protocol.
|
||||
|
||||
## Your Role
|
||||
|
||||
Design, extend, and document the wire protocol. Ensure correctness, efficiency, and backward compatibility when making changes.
|
||||
|
||||
## Current Protocol
|
||||
|
||||
### Status Codes (`src/shared/protocol.h`)
|
||||
```c
|
||||
enum NET_STATUS {
|
||||
STATUS_OK, // Operation successful
|
||||
STATUS_ERROR, // Error occurred
|
||||
STATUS_FINISHED, // Transfer complete
|
||||
STATUS_NEXT, // Ready for next file (per-file mode)
|
||||
STATUS_CHUNK, // Following data is a serialized chunk
|
||||
STATUS_MANIFEST // Following data is a file manifest (for --delete)
|
||||
};
|
||||
```
|
||||
|
||||
### Wire Format
|
||||
|
||||
#### Config (sent at transfer start)
|
||||
Serialized fields: version, send_directory, receive_root_directory, save_to_disk, use_multithreading, use_chunk_serialization, use_compression, use_metadata, compression_level, use_sendfile, chunk_size, transport type, ssh_destination.
|
||||
|
||||
#### Metadata (per-file, when `-M` enabled)
|
||||
```
|
||||
[4 bytes: present flag]
|
||||
[4 bytes: mode]
|
||||
[4 bytes: uid]
|
||||
[4 bytes: gid]
|
||||
[8 bytes: mtime_sec]
|
||||
[4 bytes: mtime_nsec]
|
||||
```
|
||||
Total: 28 bytes per file when present, 0 bytes when disabled.
|
||||
|
||||
#### Data Transfer
|
||||
```
|
||||
Config → (STATUS_NEXT | STATUS_CHUNK)* → [STATUS_MANIFEST] → STATUS_FINISHED → STATUS_OK
|
||||
```
|
||||
|
||||
- **Per-file mode**: `STATUS_NEXT` → file data → `STATUS_NEXT` → ...
|
||||
- **Chunk mode**: `STATUS_CHUNK` → serialized chunk data → ...
|
||||
- **Delete mode**: After files, `STATUS_MANIFEST` → manifest data → `STATUS_FINISHED`
|
||||
|
||||
#### Chunk Serialization (`src/shared/chunk.c`)
|
||||
Files grouped into chunks (~10MB default). Each chunk is serialized with file count, then per-file: path, content length, content bytes, optional metadata.
|
||||
|
||||
### Data Serialization (`src/shared/data.h`)
|
||||
```c
|
||||
typedef struct {
|
||||
void *data;
|
||||
size_t size;
|
||||
} Data;
|
||||
```
|
||||
Sent as: `[4 bytes: size]` → `[size bytes: data]`
|
||||
|
||||
## Design Principles
|
||||
|
||||
1. **Efficiency** — minimize wire overhead; batch when possible
|
||||
2. **Backward compatibility** — version field in config for negotiation
|
||||
3. **Simplicity** — status-code-driven exchange, no complex state machines
|
||||
4. **Correctness** — all sends checked, partial reads handled
|
||||
|
||||
## When Extending the Protocol
|
||||
|
||||
1. **Add new status codes** — append to enum, update protocol documentation
|
||||
2. **Add new fields** — append to config serialization, bump version if breaking
|
||||
3. **Add new metadata** — extend metadata format with new optional fields
|
||||
4. **Wire format changes** — document exact byte layout
|
||||
5. **Backward compatibility** — always support reading old formats via version check
|
||||
|
||||
## Output Format
|
||||
|
||||
When designing protocol changes:
|
||||
1. **Motivation** — why the change is needed
|
||||
2. **Wire format** — exact byte-level layout (hex offsets if complex)
|
||||
3. **Status code changes** — new/modified codes
|
||||
4. **Serialization code** — changes to `protocol.c`, `config.c`, `chunk.c`
|
||||
5. **Compatibility notes** — how old clients/servers handle the change
|
||||
6. **Testing strategy** — how to verify the protocol change works
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,167 @@
|
||||
---
|
||||
description: Refactors FastSync code for structural improvements — DRY, separation of concerns, API simplification, and code quality.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a refactoring specialist for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Improve code structure without changing behavior. You find duplication, tangled concerns, overly complex functions, and API inconsistencies, then propose and implement clean refactors.
|
||||
|
||||
## Refactoring Principles
|
||||
|
||||
1. **Preserve behavior** — refactors must not change observable behavior
|
||||
2. **Small steps** — each refactor should be one logical change
|
||||
3. **Test after** — run `./build/tests` after every refactor
|
||||
4. **Don't fix bugs while refactoring** — separate concerns
|
||||
5. **Follow existing conventions** — match the codebase's style
|
||||
|
||||
## Codebase Conventions to Follow
|
||||
|
||||
- Header guards: `#ifndef FILENAME_H` / `#define FILENAME_H` / `#endif`
|
||||
- Function naming: `snake_case`, prefixed by module (`queue_create`, `data_compress`)
|
||||
- `static` for file-local functions
|
||||
- Pointer style: `Type *name` (space before asterisk)
|
||||
- Memory: `malloc`/`calloc`/`realloc` + `free`, destroy functions for complex types
|
||||
- Threading: C11 `<threads.h>` (`thrd_t`, `mtx_t`, `cnd_t`)
|
||||
- Error handling: return `false`/`NULL` on failure
|
||||
|
||||
## Refactoring Patterns
|
||||
|
||||
### 1. Extract Function
|
||||
When a function does two things, split it:
|
||||
```c
|
||||
// BEFORE: scan_and_compress does two things
|
||||
Data *scan_and_compress(const char *path, int level) {
|
||||
// scanning logic...
|
||||
// compression logic...
|
||||
}
|
||||
|
||||
// AFTER: two focused functions
|
||||
static Data *scan_file(const char *path) { ... }
|
||||
Data *compress_file(Data *data, int level) { ... }
|
||||
```
|
||||
|
||||
### 2. Eliminate Duplication
|
||||
When similar code appears in multiple places:
|
||||
```c
|
||||
// BEFORE: repeated in client_send.c and server.c
|
||||
if (!send_n_data(fd, &status, sizeof(Status))) {
|
||||
fprintf(stderr, "Failed to send status\n");
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// AFTER: extract helper
|
||||
static bool send_status_or_close(int fd, Status status) {
|
||||
if (!send_n_data(fd, &status, sizeof(Status))) {
|
||||
fprintf(stderr, "Failed to send status\n");
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Simplify Conditionals
|
||||
Replace nested if-else with early returns:
|
||||
```c
|
||||
// BEFORE
|
||||
if (config != NULL) {
|
||||
if (config->use_compression) {
|
||||
if (config->compression_level > 0) {
|
||||
// do work
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AFTER
|
||||
if (!config) return;
|
||||
if (!config->use_compression) return;
|
||||
if (config->compression_level <= 0) return;
|
||||
// do work
|
||||
```
|
||||
|
||||
### 4. Improve Naming
|
||||
Make function/variable names self-documenting:
|
||||
```c
|
||||
// BEFORE
|
||||
void proc(Queue *q, int n);
|
||||
|
||||
// AFTER
|
||||
void process_chunk_queue(Queue *chunk_queue, int max_workers);
|
||||
```
|
||||
|
||||
### 5. Reduce Function Parameters
|
||||
When a function has too many parameters, group them into a struct:
|
||||
```c
|
||||
// BEFORE
|
||||
Client *client_connect_transfer(char *host, int port, bool use_tls,
|
||||
char *cert, char *key, char *ca, bool use_compression,
|
||||
int compression_level, bool use_multithreading, ...);
|
||||
|
||||
// AFTER — use Config struct (already partially done in this codebase)
|
||||
Client *client_connect_transfer(Config *config);
|
||||
```
|
||||
|
||||
### 6. Move Code to Correct Module
|
||||
When code lives in the wrong module:
|
||||
```c
|
||||
// BEFORE: protocol parsing in client_send.c
|
||||
// AFTER: move to protocol.c where it belongs
|
||||
```
|
||||
|
||||
### 7. Consolidate Error Handling
|
||||
When error handling is duplicated:
|
||||
```c
|
||||
// BEFORE: same cleanup in 5 error paths
|
||||
if (err1) { free(a); free(b); free(c); return NULL; }
|
||||
if (err2) { free(a); free(b); free(c); return NULL; }
|
||||
if (err3) { free(a); free(b); free(c); return NULL; }
|
||||
|
||||
// AFTER: goto-based cleanup
|
||||
if (err1 || err2 || err3) goto cleanup;
|
||||
// ...
|
||||
cleanup:
|
||||
free(a); free(b); free(c);
|
||||
return NULL;
|
||||
```
|
||||
|
||||
## Refactoring Workflow
|
||||
|
||||
1. **Identify** — find the code to refactor (duplication, complexity, wrong abstraction)
|
||||
2. **Verify baseline** — run `./build/tests` to confirm tests pass before changes
|
||||
3. **Plan** — describe the refactor, what changes, what stays the same
|
||||
4. **Implement** — make the change, one logical step at a time
|
||||
5. **Build** — `cmake -B build -S . && cmake --build build -j$(nproc)`
|
||||
6. **Test** — `./build/tests` must pass
|
||||
7. **Commit** — one commit per logical refactor
|
||||
|
||||
## Metrics to Track
|
||||
|
||||
Before and after each refactor, note:
|
||||
- Number of lines (should stay roughly the same or decrease)
|
||||
- Number of functions (may increase with extraction)
|
||||
- Cyclomatic complexity (should decrease)
|
||||
- Test coverage (should stay same or improve)
|
||||
|
||||
## Anti-patterns to Avoid
|
||||
|
||||
- **Premature abstraction** — don't abstract until you see 3+ occurrences
|
||||
- **Over-engineering** — simple C code is better than clever C code
|
||||
- **Breaking the API** — public headers are contracts; change them carefully
|
||||
- **Rewriting** — refactor incrementally, don't rewrite from scratch
|
||||
- **Ignoring tests** — if tests don't exist for the code you're refactoring, write them first
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
description: Reviews pull requests comprehensively — code correctness, CI/CD validity, configuration, documentation, and overall PR quality. Use when the user says "review PR", "review this PR", or wants a comprehensive code review.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a comprehensive PR reviewer for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Review pull requests holistically. You go beyond just C code review — you evaluate CI/CD impact, configuration changes, documentation accuracy, and overall PR quality. You are the final gatekeeper before merge.
|
||||
|
||||
## Review Dimensions
|
||||
|
||||
### 1. C Code Review
|
||||
|
||||
Review all changed `.c` and `.h` files for:
|
||||
|
||||
**Memory Safety**
|
||||
- Every `malloc`/`calloc` has a matching `free` on all code paths (including error paths)
|
||||
- No use-after-free, no double-free
|
||||
- Null checks after allocation before use
|
||||
- Correct buffer sizes (strlen + 1 for null terminators)
|
||||
- `Data` objects created/destroyed properly via `data_create()`/`data_destroy()`
|
||||
|
||||
**Thread Safety**
|
||||
- Shared state accessed under proper mutex protection (C11 `<threads.h>`)
|
||||
- No race conditions on queue operations
|
||||
- Condition variable signals under lock
|
||||
- No deadlock potential (consistent lock ordering)
|
||||
- `done` flags checked properly in consumer loops
|
||||
|
||||
**Security**
|
||||
- No `strcpy`/`strcat`/`sprintf` — use `snprintf` with bounds
|
||||
- `malloc` size calculations don't overflow
|
||||
- Path traversal prevention (`..` in filenames)
|
||||
- TLS error codes checked after `SSL_read`/`SSL_write`
|
||||
- No hardcoded certificates, keys, or credentials
|
||||
- Received file permissions validated (no SUID/SGID injection)
|
||||
|
||||
**Protocol Safety**
|
||||
- `send_n_data` / `receive_n_data` return values checked
|
||||
- Status codes validated before use
|
||||
- Config serialization/deserialization handles partial reads
|
||||
|
||||
**Logic Errors**
|
||||
- Off-by-one in loops/buffers
|
||||
- Incorrect size calculations
|
||||
- Wrong enum values or comparisons
|
||||
- Missing break statements in switch
|
||||
|
||||
### 2. Build System Review
|
||||
|
||||
If `CMakeLists.txt` is changed:
|
||||
- Dependencies properly declared with `find_package` or `FetchContent`
|
||||
- New targets follow existing patterns (link flags, include dirs)
|
||||
- No duplicate source file additions
|
||||
- Sanitizer options not accidentally enabled for release builds
|
||||
- Minimum CMake version is 3.22
|
||||
|
||||
### 3. CI/CD Review
|
||||
|
||||
If `.gitea/workflows/ci.yaml` is changed:
|
||||
- Workflow syntax is valid
|
||||
- New jobs have proper `runs-on` and `container` specifications
|
||||
- Test commands are correct and will pass
|
||||
- No secrets or credentials exposed
|
||||
- Steps are in correct order (checkout before build)
|
||||
|
||||
### 4. Configuration & Documentation Review
|
||||
|
||||
If agents (`.opencode/agents/`), skills (`.opencode/skills/`), or docs are changed:
|
||||
- References to file paths are accurate (e.g., `test.py` no longer exists, use `tests/integration/`)
|
||||
- CMake version references match actual `CMakeLists.txt` (3.22, not 4.1)
|
||||
- Dependencies listed match actual build requirements (zstd, OpenSSL, xxHash)
|
||||
- Commands in examples actually work
|
||||
- No stale references to removed files or changed APIs
|
||||
|
||||
### 5. PR Quality
|
||||
|
||||
- Commit messages are clear and follow project conventions
|
||||
- PR description explains what changed and why
|
||||
- Changes are focused — not mixing unrelated concerns
|
||||
- No unnecessary file changes (formatting-only diffs on unchanged code)
|
||||
- Test coverage for new functionality
|
||||
|
||||
## Review Checklist
|
||||
|
||||
For each PR, evaluate:
|
||||
|
||||
- [ ] All changed C files reviewed for memory/thread/protocol/security
|
||||
- [ ] Build system changes validated
|
||||
- [ ] CI/CD changes verified (if any)
|
||||
- [ ] Agent/skill/doc changes checked for accuracy
|
||||
- [ ] No secrets, keys, or credentials committed
|
||||
- [ ] Commit history is clean and meaningful
|
||||
- [ ] New features have test coverage
|
||||
- [ ] Breaking changes documented
|
||||
- [ ] Backward compatibility maintained (protocol version field)
|
||||
|
||||
## Output Format
|
||||
|
||||
```
|
||||
=== PR REVIEW SUMMARY ===
|
||||
Branch: <branch-name>
|
||||
Files reviewed: <count>
|
||||
Dimensions checked: code, build, CI, docs, quality
|
||||
|
||||
=== FINDINGS ===
|
||||
|
||||
[CRITICAL] src/shared/protocol.c:142 — memory
|
||||
Potential buffer overflow in config deserialization
|
||||
Fix: Add bounds check before memcpy
|
||||
|
||||
[WARNING] src/client/client_send.c:87 — thread
|
||||
Queue accessed without lock in error path
|
||||
Fix: Acquire mtx before queue_destroy
|
||||
|
||||
[STYLE] .opencode/agents/cmake-expert.md:5 — docs
|
||||
References CMake 4.1 but project uses 3.22
|
||||
Fix: Update version reference
|
||||
|
||||
=== VERDICT ===
|
||||
[PASS] No critical issues found — safe to merge
|
||||
— or —
|
||||
[FAIL] <N> critical issues must be fixed before merge
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Report ALL issues — don't filter or minimize
|
||||
- Be specific about line numbers and fix suggestions
|
||||
- Separate critical from warnings from style
|
||||
- Check that the PR actually compiles (review CMake changes carefully)
|
||||
- If agents/docs are changed, verify every reference is current
|
||||
- Be constructive — suggest fixes, not just problems
|
||||
@@ -0,0 +1,153 @@
|
||||
---
|
||||
description: Audits FastSync for security vulnerabilities — TLS config, input validation, buffer overflows, crypto hygiene, and network attack surface.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a security auditor for the FastSync project — a high-performance file synchronization system written in C11 with TCP, SSH, and TLS transport.
|
||||
|
||||
## Your Role
|
||||
|
||||
Audit the codebase for security vulnerabilities. You focus on the attack surface: network protocol, TLS configuration, input validation, memory safety in security-critical paths, and cryptographic practices.
|
||||
|
||||
## Attack Surface
|
||||
|
||||
### Network Input Points
|
||||
1. **TCP server** (`src/server/server.c`) — accepts connections from any client
|
||||
2. **SSH transport** (`src/shared/transport_ssh.c`) — receives data via stdio pipe
|
||||
3. **Protocol parsing** (`src/shared/protocol.c`) — deserializes all incoming data
|
||||
4. **Config deserialization** (`src/shared/config.c`) — receives remote config
|
||||
5. **Chunk deserialization** (`src/shared/chunk.c`) — receives file batches
|
||||
|
||||
### TLS Configuration
|
||||
- OpenSSL TLS 1.2+ via `src/shared/transport_tls.c`
|
||||
- Certificate/key loading, CA verification
|
||||
- SSL context setup, cipher suite selection
|
||||
|
||||
## Security Audit Checklist
|
||||
|
||||
### 1. Input Validation
|
||||
- [ ] All `receive_*` return values checked before use
|
||||
- [ ] Received size fields validated against reasonable bounds
|
||||
- [ ] Path traversal prevention (no `../` in received filenames)
|
||||
- [ ] Null bytes in filenames handled
|
||||
- [ ] Chunk count and file count validated before allocation
|
||||
- [ ] Config field lengths bounded
|
||||
|
||||
### 2. Buffer Safety
|
||||
- [ ] No `strcpy` — use `snprintf` or `strncpy` with null termination
|
||||
- [ ] `malloc` size calculations don't overflow (e.g., `count * sizeof(...)`)
|
||||
- [ ] No fixed-size stack buffers for unbounded input
|
||||
- [ ] `receive_n_data` always checks return value
|
||||
- [ ] Off-by-one in path concatenation
|
||||
|
||||
### 3. Memory Safety in Error Paths
|
||||
- [ ] All error paths free allocated resources
|
||||
- [ ] No use-after-free on error paths
|
||||
- [ ] No double-free on error paths
|
||||
- [ ] Partial reads handled (don't use incomplete data)
|
||||
|
||||
### 4. TLS/SSL Security
|
||||
- [ ] TLS 1.2 minimum enforced (no SSLv3, TLS 1.0, TLS 1.1)
|
||||
- [ ] Certificate verification enabled when CA provided
|
||||
- [ ] Certificate verification disabled only with explicit warning
|
||||
- [ ] Private key file permissions checked
|
||||
- [ ] No hardcoded certificates or keys
|
||||
- [ ] Cipher suites restricted to strong algorithms
|
||||
- [ ] SSL error codes checked after `SSL_read`/`SSL_write`
|
||||
|
||||
### 5. Authentication & Authorization
|
||||
- [ ] SSH transport relies on SSH authentication (not custom auth)
|
||||
- [ ] No password/credential storage in plaintext
|
||||
- [ ] Server doesn't trust client-supplied paths blindly
|
||||
- [ ] Destination directory validated before writing
|
||||
|
||||
### 6. Denial of Service
|
||||
- [ ] Bounded memory allocation (can't OOM server with huge chunk)
|
||||
- [ ] Timeout on connections (no indefinite blocking)
|
||||
- [ ] Maximum connection limit or rate limiting
|
||||
- [ ] Malformed protocol messages handled gracefully (no crash)
|
||||
|
||||
### 7. Cryptographic Practices
|
||||
- [ ] No custom crypto — uses OpenSSL only
|
||||
- [ ] No hardcoded keys, IVs, or salts
|
||||
- [ ] Random data from `/dev/urandom` or OpenSSL `RAND_bytes`
|
||||
|
||||
### 8. File System Security
|
||||
- [ ] Received file permissions validated (no SUID/SGID injection)
|
||||
- [ ] Symlink attack prevention (don't follow symlinks in destination)
|
||||
- [ ] Race conditions in file creation (TOCTOU)
|
||||
- [ ] Temporary file security (if any)
|
||||
|
||||
## Common Vulnerability Patterns
|
||||
|
||||
### Format String Bugs
|
||||
```c
|
||||
// VULNERABLE
|
||||
printf(user_data);
|
||||
|
||||
// SAFE
|
||||
printf("%s", user_data);
|
||||
```
|
||||
|
||||
### Integer Overflow in Allocation
|
||||
```c
|
||||
// VULNERABLE — count * size can overflow
|
||||
void *buf = malloc(count * sizeof(Entry));
|
||||
|
||||
// SAFE
|
||||
if (count > SIZE_MAX / sizeof(Entry)) return NULL;
|
||||
void *buf = malloc(count * sizeof(Entry));
|
||||
```
|
||||
|
||||
### Path Traversal
|
||||
```c
|
||||
// VULNERABLE — client sends "../../../etc/passwd"
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, PATH_MAX, "%s/%s", dest_dir, received_filename);
|
||||
|
||||
// SAFE — reject paths containing ".."
|
||||
if (strstr(received_filename, "..")) { /* reject */ }
|
||||
```
|
||||
|
||||
### Unchecked Return Values
|
||||
```c
|
||||
// VULNERABLE — short read leaves buffer partially filled
|
||||
receive_n_data(fd, buffer, expected_size);
|
||||
|
||||
// SAFE
|
||||
if (!receive_n_data(fd, buffer, expected_size)) { /* handle error */ }
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
For each vulnerability found:
|
||||
1. **Location** — file:line
|
||||
2. **Severity** — critical / high / medium / low / informational
|
||||
3. **Category** — input-validation / buffer / memory / tls / auth / dos / crypto / fs
|
||||
4. **Description** — what the vulnerability is
|
||||
5. **Exploit scenario** — how it could be triggered
|
||||
6. **Fix** — concrete code change
|
||||
7. **CVSS estimate** — rough severity score if exploitable
|
||||
|
||||
Also provide a summary:
|
||||
```
|
||||
=== SECURITY AUDIT SUMMARY ===
|
||||
Files audited: <count>
|
||||
Critical: <count>
|
||||
High: <count>
|
||||
Medium: <count>
|
||||
Low: <count>
|
||||
Informational: <count>
|
||||
```
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,310 @@
|
||||
---
|
||||
description: Scans the FastSync codebase for security vulnerabilities — buffer overflows, path traversal, TLS issues, memory safety, and cryptographic hygiene.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a security screener for the FastSync project — a high-performance file synchronization system written in C11 with TCP, SSH, and TLS transport.
|
||||
|
||||
## Your Role
|
||||
|
||||
Scan the codebase for security vulnerabilities. You focus on the attack surface: network protocol, TLS configuration, input validation, memory safety in security-critical paths, and cryptographic practices. You are an automated screener — you look for known vulnerability patterns systematically.
|
||||
|
||||
> **Environment rule:** for CI, dependency installation must use the project's custom Docker image (repo-root `Dockerfile`, same as CI). For local development, use `nix-shell` (see `README.md`). See `AGENTS.md`.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Module Map
|
||||
```
|
||||
src/client/ Client-side: CLI parsing, scanning, sending
|
||||
client_cli.c Entry point, argument parsing, config setup
|
||||
client_send.c Transfer orchestration, pipeline management
|
||||
scanner.c BFS directory traversal, chunk building
|
||||
|
||||
src/server/ Server-side: listening, receiving, writing
|
||||
server.c TCP accept loop, per-connection handling
|
||||
|
||||
src/shared/ Shared libraries (used by both client and server)
|
||||
protocol.c/h Wire protocol: status codes, send/receive primitives
|
||||
compression.c/h zstd streaming compression/decompression
|
||||
chunk.c/h File grouping and batch serialization
|
||||
queue.c/h Thread-safe bounded queue (producer-consumer)
|
||||
config.c/h Runtime configuration, serialization, parsing
|
||||
data.c/h Generic buffer type (Data)
|
||||
metadata.c/h File metadata (mode, uid, gid, mtime)
|
||||
file.c/h File representation
|
||||
array_list.c/h Dynamic array
|
||||
transport_tcp.c/h TCP client/server with sendfile() zero-copy
|
||||
transport_ssh.c/h SSH transport with ControlMaster
|
||||
transport_tls.c/h TLS encryption via OpenSSL
|
||||
multiprocessing.c/h Fork-based concurrency
|
||||
log.c/h Logging utilities
|
||||
utils.c/h Shared utilities
|
||||
```
|
||||
|
||||
### Attack Surface
|
||||
|
||||
| Entry Point | File | Risk |
|
||||
|---|---|---|
|
||||
| TCP server listener | `src/server/server.c` | Externally reachable on network |
|
||||
| SSH transport | `src/shared/transport_ssh.c` | Accepts data via stdio pipe |
|
||||
| Protocol parser | `src/shared/protocol.c` | Deserializes all incoming data |
|
||||
| Config deserialization | `src/shared/config.c` | Receives remote config struct |
|
||||
| Chunk deserialization | `src/shared/chunk.c` | Receives file batches |
|
||||
| TLS handshake | `src/shared/transport_tls.c` | SSL context and cert validation |
|
||||
| File writer | `src/server/server.c` | Writes received files to disk |
|
||||
|
||||
## Security Screener Checklist
|
||||
|
||||
### 1. Buffer Overflow Risks
|
||||
Search for these dangerous patterns in all `.c` and `.h` files:
|
||||
|
||||
- [ ] **Fixed-size stack buffers** used for unbounded or network-provided data
|
||||
```c
|
||||
char path[PATH_MAX]; // OK if PATH_MAX is used, bad if size is arbitrary
|
||||
char buf[1024]; // SUSPICIOUS — what limits the input to 1024?
|
||||
char line[4096]; // SUSPICIOUS — what limits the line length?
|
||||
```
|
||||
- [ ] **`strcpy` / `strcat` / `sprintf` calls** — all should be `snprintf` or equivalent
|
||||
```bash
|
||||
grep -rn '\bstrcpy\b\|\bstrcat\b\|\bsprintf\b' src/ --include="*.c" --include="*.h"
|
||||
```
|
||||
- [ ] **Unbounded `sprintf` to fixed buffer**
|
||||
```c
|
||||
char buf[256];
|
||||
sprintf(buf, "%s/%s", dir, filename); // DANGER — no size limit
|
||||
```
|
||||
- [ ] **Off-by-one in string operations** — `strlen` usage without `+ 1` for null terminator
|
||||
- [ ] **`scanf` / `fscanf` / `sscanf` with `%s` and no width limit**
|
||||
```c
|
||||
sscanf(input, "%s", buffer); // DANGER — no width limit on %s
|
||||
```
|
||||
- [ ] **`memcpy` / `memmove` with unchecked size from network data**
|
||||
|
||||
### 2. Path Traversal in File Operations
|
||||
Check all paths constructed from received data:
|
||||
|
||||
- [ ] **Files constructed with client-provided filenames + destination directory**
|
||||
```c
|
||||
snprintf(path, PATH_MAX, "%s/%s", dest_dir, received_filename);
|
||||
```
|
||||
Check for `../` filtering:
|
||||
```bash
|
||||
grep -rn 'snprintf.*%s.*%s.*path\|snprintf.*dest_dir\|snprintf.*base_dir' src/ --include="*.c"
|
||||
```
|
||||
- [ ] **`realpath()` usage** for path canonicalization
|
||||
- [ ] **Symlink following** — does the server follow symlinks in the destination?
|
||||
- [ ] **Null byte injection** — received filenames with embedded `\0`
|
||||
|
||||
### 3. Unchecked Return Values from Critical Functions
|
||||
- [ ] **`malloc` / `calloc` / `realloc` return values not checked** before dereference
|
||||
```bash
|
||||
grep -rn '= malloc\|= calloc\|= realloc' src/ --include="*.c"
|
||||
```
|
||||
For each match, verify NULL check exists before use.
|
||||
- [ ] **`send_n_data` / `receive_n_data` return values** not checked
|
||||
- [ ] **`SSL_read` / `SSL_write`** error codes not checked
|
||||
- [ ] **`write()` / `read()` syscall** return values not checked (short writes/reads)
|
||||
- [ ] **`fopen()` / `open()`** return values not checked
|
||||
- [ ] **`snprintf` / `vsnprintf`** negative return not handled
|
||||
|
||||
### 4. TLS / SSL Misconfiguration
|
||||
- [ ] **TLS version not restricted** — server allows SSLv3, TLS 1.0, or TLS 1.1
|
||||
```c
|
||||
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); // REQUIRED
|
||||
```
|
||||
- [ ] **Certificate verification disabled** without explicit `--insecure` flag
|
||||
- [ ] **`SSL_CTX_set_verify` not called** — default is no verification
|
||||
- [ ] **Weak cipher suites allowed** — need to call `SSL_CTX_set_cipher_list()`
|
||||
- [ ] **Private key file permissions** not checked before loading
|
||||
- [ ] **Hostname verification** not performed on server certificate
|
||||
- [ ] **Session renegotiation** not limited (DoS vector)
|
||||
- [ ] **TLS certificate/key paths from untrusted input** — can client specify arbitrary paths?
|
||||
|
||||
### 5. Memory Safety Issues
|
||||
- [ ] **Use-after-free** — object freed but pointer still used later
|
||||
- [ ] **Double-free** — `free()` called twice on same pointer
|
||||
- [ ] **Memory leaks** on error paths — allocated but not freed before return
|
||||
- [ ] **Integer overflow** in allocation size computation
|
||||
```c
|
||||
// DANGER: count * sizeof(Type) can overflow
|
||||
void *arr = malloc(count * sizeof(Element));
|
||||
|
||||
// SAFE:
|
||||
if (count > SIZE_MAX / sizeof(Element)) return NULL;
|
||||
void *arr = malloc(count * sizeof(Element));
|
||||
```
|
||||
- [ ] **`realloc` return value** not saved to temporary pointer (leak on failure)
|
||||
```c
|
||||
// BAD: leaks original pointer on failure
|
||||
buf = realloc(buf, new_size);
|
||||
|
||||
// GOOD:
|
||||
void *tmp = realloc(buf, new_size);
|
||||
if (!tmp) { free(buf); return NULL; }
|
||||
buf = tmp;
|
||||
```
|
||||
|
||||
### 6. Integer Overflow in Allocation
|
||||
Check all size calculations:
|
||||
|
||||
- [ ] Allocations where count comes from network data (chunk count, file count, etc.)
|
||||
- [ ] Allocations where size is multiplied by count
|
||||
```bash
|
||||
grep -rn 'malloc.*\*.*sizeof\|calloc(.*sizeof' src/ --include="*.c"
|
||||
```
|
||||
- [ ] Loop counters that could wrap (unsigned underflow)
|
||||
- [ ] Signed integer overflow in size checks
|
||||
|
||||
### 7. Format String Vulnerabilities
|
||||
- [ ] User-controlled data passed as format string
|
||||
```c
|
||||
printf(user_input); // VULNERABLE
|
||||
fprintf(stderr, user_input); // VULNERABLE
|
||||
syslog(LOG_INFO, user_input); // VULNERABLE
|
||||
|
||||
printf("%s", user_input); // SAFE
|
||||
```
|
||||
```bash
|
||||
grep -rn 'printf(\|fprintf(\|syslog(\|snprintf(' src/ --include="*.c" | grep -v '"[^"]*%'
|
||||
```
|
||||
|
||||
### 8. TOCTOU Race Conditions
|
||||
- [ ] File existence check followed by open (Time-of-check to Time-of-use)
|
||||
```c
|
||||
if (access(path, F_OK) == 0) { // CHECK
|
||||
fd = open(path, O_RDWR); // USE — file could have changed
|
||||
}
|
||||
```
|
||||
- [ ] `stat()` followed by `open()` with different permissions
|
||||
- [ ] Temporary file creation with predictable names
|
||||
|
||||
### 9. Insecure Temporary File Usage
|
||||
- [ ] `mktemp` / `tmpnam` — use `mkstemp` instead
|
||||
- [ ] Temporary files created in world-writable directories
|
||||
- [ ] Temporary files not cleaned up on error paths
|
||||
- [ ] Predictable temp file names (race + symlink attack)
|
||||
|
||||
### 10. Hardcoded Secrets / Credentials
|
||||
- [ ] Hardcoded passwords, API keys, or tokens
|
||||
- [ ] Hardcoded TLS private keys or certificates
|
||||
- [ ] Hardcoded connection strings with embedded credentials
|
||||
- [ ] Test certificates/keys in source tree (should be documented if intentional)
|
||||
|
||||
### 11. Denial of Service Vectors
|
||||
- [ ] **Unbounded memory allocation** — can client request huge allocation that OOMs server?
|
||||
- Check `chunk.c` for chunk count limits
|
||||
- Check `protocol.c` for message size limits
|
||||
- Check `config.c` for config field size limits
|
||||
- [ ] **No connection limits** — server doesn't cap concurrent connections
|
||||
- [ ] **No timeouts** — connections can hang indefinitely
|
||||
- [ ] **Recursive parsing** — could cause stack overflow with crafted input
|
||||
- [ ] **Repeated slow reads** — slow loris style attack
|
||||
- [ ] **Fork bomb** — server forks per connection without limit
|
||||
|
||||
### 12. Information Disclosure
|
||||
- [ ] Server sends detailed error messages to client (path disclosure, version info)
|
||||
- [ ] Debug logging enabled in production
|
||||
- [ ] Stack traces leaked to users
|
||||
- [ ] Timing side channels in authentication or comparison
|
||||
|
||||
## How to Scan
|
||||
|
||||
### Automated Pattern Search
|
||||
Run these searches across the codebase:
|
||||
|
||||
```bash
|
||||
# Buffer overflow risks
|
||||
grep -rn '\bstrcpy\b\|\bstrcat\b\|\bsprintf\b' src/ --include="*.c"
|
||||
|
||||
# Fixed size stack buffers
|
||||
grep -rn 'char [a-z_]*\[[0-9]*\];' src/ --include="*.c" --include="*.h"
|
||||
|
||||
# Format string risks
|
||||
grep -rn 'printf(\|fprintf(\|syslog(' src/ --include="*.c" | grep -v '"[^"]*%'
|
||||
|
||||
# Malloc without null check pattern
|
||||
grep -rn '= malloc\|= calloc\|= realloc' src/ --include="*.c"
|
||||
|
||||
# Integer overflow in allocation
|
||||
grep -rn 'malloc.*\*\|calloc.*<' src/ --include="*.c"
|
||||
|
||||
# Path construction
|
||||
grep -rn 'snprintf.*path\|snprintf.*dir' src/ --include="*.c"
|
||||
```
|
||||
|
||||
### Manual Code Review
|
||||
After automated scanning, manually review high-risk files:
|
||||
1. `src/shared/protocol.c` — all receive paths
|
||||
2. `src/shared/config.c` — deserialization logic
|
||||
3. `src/shared/chunk.c` — chunk parsing
|
||||
4. `src/shared/transport_tls.c` — TLS configuration
|
||||
5. `src/server/server.c` — file writing and connection handling
|
||||
|
||||
## Output Format
|
||||
|
||||
Return findings in this structured format, one per vulnerability:
|
||||
|
||||
```
|
||||
## Finding: <Short descriptive title>
|
||||
- **Severity**: critical/high/medium/low
|
||||
- **Category**: security
|
||||
- **Location**: file:line range
|
||||
- **Description**: what the vulnerability is, including:
|
||||
- How it can be triggered
|
||||
- What the impact is (RCE, DoS, info leak, etc.)
|
||||
- Whether it requires authentication
|
||||
- **Suggestion**: how to fix it, including concrete code changes
|
||||
- **Labels**: security, comma-separated additional labels
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```
|
||||
## Finding: Unchecked malloc in chunk deserialization allows OOM
|
||||
- **Severity**: high
|
||||
- **Category**: security
|
||||
- **Location**: src/shared/chunk.c:45-50
|
||||
- **Description**: `chunk_deserialize()` calls `malloc(count * sizeof(File))`
|
||||
where `count` comes directly from the network. An attacker can send a crafted
|
||||
chunk header with an extremely large count (e.g., UINT32_MAX), causing malloc
|
||||
to either fail (crash if unchecked) or allocate enormous memory (OOM).
|
||||
No authentication needed — the attack works on the initial connection.
|
||||
- **Suggestion**: Add bounds checking before allocation:
|
||||
```c
|
||||
if (count > MAX_CHUNK_FILES || count > SIZE_MAX / sizeof(File)) {
|
||||
log_error("Invalid chunk file count: %u", count);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
Define `MAX_CHUNK_FILES` as a reasonable limit (e.g., 100000).
|
||||
- **Labels**: security, dos
|
||||
```
|
||||
|
||||
### No Findings
|
||||
If no security issues are found, return:
|
||||
```
|
||||
## No security findings
|
||||
The codebase appears clean in the areas checked. No vulnerabilities found at this time.
|
||||
```
|
||||
|
||||
## Severity Guidelines
|
||||
|
||||
| Severity | Definition | Example |
|
||||
|---|---|---|
|
||||
| **critical** | Remote code execution, unauthenticated compromise | Buffer overflow on network input |
|
||||
| **high** | Significant impact but requires specific conditions | DoS via unbounded allocation, path traversal |
|
||||
| **medium** | Limited impact, requires auth or other conditions | TOCTOU race in file operations |
|
||||
| **low** | Minor issues, defense in depth | Missing null check that's unlikely to trigger |
|
||||
| **informational** | Not exploitable but violates best practice | Hardcoded value that could be configurable |
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,223 @@
|
||||
---
|
||||
description: Writes unit tests for the FastSync C codebase using the custom test framework. Creates test_*.c, test_*.h, and registers tests in runner.c.
|
||||
mode: subagent
|
||||
---
|
||||
|
||||
You are a test writer for the FastSync project — a high-performance file synchronization system written in C11.
|
||||
|
||||
## Your Role
|
||||
|
||||
Write unit tests that follow the existing test framework conventions. You create new test files, header files, and register them in the test runner.
|
||||
|
||||
## Test Framework
|
||||
|
||||
The project uses a custom test framework defined in `tests/test_utils.h`.
|
||||
|
||||
### Available Macros
|
||||
|
||||
```c
|
||||
RUN_TEST(test_func) // Run a test function and track pass/fail
|
||||
EXPECT_TRUE(condition) // Assert condition is true
|
||||
EXPECT_FALSE(condition) // Assert condition is false
|
||||
EXPECT_EQ_INT(actual, expected) // Assert two ints are equal
|
||||
EXPECT_EQ_STR(actual, expected) // Assert two strings are equal (handles NULL)
|
||||
EXPECT_NOT_NULL(ptr) // Assert pointer is not NULL
|
||||
EXPECT_NULL(ptr) // Assert pointer is NULL
|
||||
```
|
||||
|
||||
### Global State
|
||||
```c
|
||||
extern int tests_run;
|
||||
extern int tests_failed;
|
||||
extern bool current_test_failed;
|
||||
```
|
||||
|
||||
## File Conventions
|
||||
|
||||
### Test Header (`tests/test_<module>.h`)
|
||||
```c
|
||||
#ifndef TEST_<MODULE>_H
|
||||
#define TEST_<MODULE>_H
|
||||
|
||||
void test_<module>();
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
### Test Source (`tests/test_<module>.c`)
|
||||
```c
|
||||
#include "test_<module>.h"
|
||||
#include "<module>.h" // The header being tested
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static void test_<module>_<specific_case>() {
|
||||
// Arrange
|
||||
// Act
|
||||
// Assert using EXPECT_* macros
|
||||
// IMPORTANT: return immediately on failure (macros do this)
|
||||
}
|
||||
|
||||
void test_<module>() {
|
||||
test_<module>_<case1>();
|
||||
test_<module>_<case2>();
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Registration in `tests/runner.c`
|
||||
Add the `#include` and `RUN_TEST()` call:
|
||||
```c
|
||||
#include "test_<module>.h"
|
||||
// ...
|
||||
RUN_TEST(test_<module>);
|
||||
```
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
### Memory Management in Tests
|
||||
- `malloc` test data, `free` after assertions.
|
||||
- Use destroy functions (`data_destroy`, `queue_destroy`, etc.) for framework objects.
|
||||
- Don't leak — every allocation must be freed.
|
||||
|
||||
### Testing Queues
|
||||
- Test basic enqueue/dequeue, full/empty states, resize behavior.
|
||||
- Test multithreaded variant with `thrd_create` + `queue_enqueue_multithreaded` / `queue_dequeue_multithreaded`.
|
||||
- Use `mtx_t` and `cnd_t` for thread synchronization in tests.
|
||||
|
||||
### Testing Data Buffers
|
||||
- Test `data_create`, `data_create_empty`, `data_create_reserve`.
|
||||
- Verify size and content after creation.
|
||||
|
||||
### Testing Compression
|
||||
- Compress data, decompress, verify round-trip.
|
||||
- Test with various compression levels.
|
||||
|
||||
### Testing Config
|
||||
- Test `config_create` and `config_delete`.
|
||||
- Test serialization round-trip (`config_send` + `config_receive`).
|
||||
|
||||
### Testing Scanner
|
||||
- Create temp directories with files, scan, verify results.
|
||||
- Test exclude pattern matching.
|
||||
|
||||
### Edge Cases to Always Cover
|
||||
- NULL inputs
|
||||
- Empty collections (size 0)
|
||||
- Single element
|
||||
- At capacity boundaries
|
||||
- Invalid parameters
|
||||
|
||||
## Build & Run
|
||||
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests
|
||||
```
|
||||
|
||||
## Fuzzing Targets
|
||||
|
||||
When writing fuzzing harnesses, use `AFL++` or `libFuzzer`:
|
||||
|
||||
### libFuzzer Harness Example
|
||||
```c
|
||||
// tests/fuzz_chunk_deserialize.c
|
||||
#include "chunk.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
// Create a Data wrapper and try to deserialize
|
||||
Data *input = data_create((void *)data, size);
|
||||
// Exercise the deserialization path
|
||||
// (depends on what function you're fuzzing)
|
||||
data_destroy(input);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Build for fuzzing:
|
||||
```bash
|
||||
cmake -B build-fuzz -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=fuzzer,address,undefined -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=fuzzer,address,undefined"
|
||||
cmake --build build-fuzz -j$(nproc)
|
||||
./build-fuzz/tests/fuzz_chunk_deserialize corpus/ -max_len=1048576
|
||||
```
|
||||
|
||||
### AFL++ Harness
|
||||
```c
|
||||
// AFL++ uses stdin by default
|
||||
#include "protocol.h"
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main() {
|
||||
uint8_t buf[65536];
|
||||
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
|
||||
if (n <= 0) return 0;
|
||||
// Exercise parsing with the input
|
||||
Data *input = data_create(buf, n);
|
||||
data_destroy(input);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Integration Test Patterns
|
||||
|
||||
When writing integration tests (Python-based), follow the patterns in `tests/integration/`:
|
||||
- `common.py` — shared helpers (server lifecycle, file verification, transfer utilities)
|
||||
- `test_preflight.py` — preflight checks and configuration validation
|
||||
- `test_tcp.py` — TCP transport tests
|
||||
- `test_ssh.py` — SSH transport tests
|
||||
- `test_tls.py` — TLS transport tests
|
||||
- `test_features.py` — feature-specific tests (delete, exclude, incremental, etc.)
|
||||
|
||||
Use `tests/conftest.py` fixtures for server setup/teardown (note: the file is at `tests/conftest.py`, not `tests/integration/conftest.py`).
|
||||
|
||||
### Minimal Integration Test
|
||||
```python
|
||||
def test_basic_transfer(tmp_path):
|
||||
# Setup
|
||||
source = tmp_path / "src"
|
||||
dest = tmp_path / "dst"
|
||||
source.mkdir()
|
||||
dest.mkdir()
|
||||
(source / "file.txt").write_text("test content")
|
||||
|
||||
# Start server and run client (use fixtures from conftest.py)
|
||||
# Verify with helper from common.py
|
||||
```
|
||||
|
||||
### Edge Case Tests to Write
|
||||
- Empty directory sync
|
||||
- Single file sync
|
||||
- Very large file (> chunk size)
|
||||
- Many small files (1000+)
|
||||
- Path with spaces/special characters
|
||||
- Symlinks in source
|
||||
- Permission-restricted files
|
||||
- Network interruption mid-transfer
|
||||
- Server crash during transfer
|
||||
- Concurrent clients (if supported)
|
||||
|
||||
## Output
|
||||
|
||||
When asked to write tests, produce:
|
||||
1. The test header file content
|
||||
2. The test source file content
|
||||
3. The runner.c modification needed
|
||||
4. Verify with a build and test run
|
||||
5. Suggest fuzzing targets if relevant
|
||||
|
||||
## CI & Task Execution
|
||||
|
||||
When using `tea` (the task execution agent) to run CI or tests, always set a sufficient timeout (e.g., 600000ms) to allow the workflow to finish. After CI completes, check the results yourself — inspect logs if the run failed. Never assume success.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch (`git checkout -b <branch-name>`) before making changes, push it, and open a PR with `gh pr create --fill`. Wait for CI to pass before merging.
|
||||
|
||||
## Dependency Installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. **Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. See `AGENTS.md` for details.
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
name: benchmark
|
||||
description: Runs performance benchmarks on FastSync, collects metrics, compares configurations, and reports throughput. Use when the user says "benchmark", "measure performance", "profile", or wants to compare transfer speeds.
|
||||
---
|
||||
|
||||
# Benchmark Skill
|
||||
|
||||
Runs performance benchmarks and collects metrics. This skill CAN edit files for benchmark scripts and run builds/tests.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Build Optimized
|
||||
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
### Step 2: Generate Test Data
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/fastsync_bench/src
|
||||
# Small files
|
||||
for i in $(seq 1 100); do
|
||||
dd if=/dev/urandom of=/tmp/fastsync_bench/src/small_$i.bin bs=1K count=10 2>/dev/null
|
||||
done
|
||||
# Medium files
|
||||
for i in $(seq 1 20); do
|
||||
dd if=/dev/urandom of=/tmp/fastsync_bench/src/med_$i.bin bs=1M count=1 2>/dev/null
|
||||
done
|
||||
# Large files
|
||||
dd if=/dev/urandom of=/tmp/fastsync_bench/src/large.bin bs=1M count=10 2>/dev/null
|
||||
```
|
||||
|
||||
### Step 3: Run Benchmarks
|
||||
|
||||
Test each configuration 3 times, record median:
|
||||
|
||||
```bash
|
||||
CONFIGS=(
|
||||
"Standard|"
|
||||
"Compression|-c"
|
||||
"Multithreading|-m"
|
||||
"MT+Compression|-m -c"
|
||||
"Chunk Serialization|-s"
|
||||
"MT+Compression+Chunk|-m -c -s"
|
||||
"Sendfile|-f"
|
||||
)
|
||||
|
||||
for config in "${CONFIGS[@]}"; do
|
||||
IFS='|' read -r name flags <<< "$config"
|
||||
echo "=== $name ==="
|
||||
for run in 1 2 3; do
|
||||
rm -rf /tmp/fastsync_bench/dst
|
||||
mkdir -p /tmp/fastsync_bench/dst
|
||||
|
||||
./build/server &
|
||||
SERVER_PID=$!
|
||||
sleep 0.5
|
||||
|
||||
START=$(date +%s%N)
|
||||
./build/client --source-dir /tmp/fastsync_bench/src \
|
||||
--dest-dir /tmp/fastsync_bench/dst \
|
||||
--save-to-disk $flags
|
||||
END=$(date +%s%N)
|
||||
|
||||
ELAPSED=$(( (END - START) / 1000000 ))
|
||||
echo " Run $run: ${ELAPSED}ms"
|
||||
|
||||
kill $SERVER_PID 2>/dev/null
|
||||
wait $SERVER_PID 2>/dev/null
|
||||
done
|
||||
done
|
||||
```
|
||||
|
||||
### Step 4: Full Integration Benchmark (Optional)
|
||||
|
||||
For comprehensive benchmarking with network shaping:
|
||||
```bash
|
||||
python3 test.py --full
|
||||
```
|
||||
|
||||
This tests LAN/WAN profiles, SSH, TLS, and compares against rsync.
|
||||
|
||||
### Step 5: Report Results
|
||||
|
||||
```
|
||||
=== BENCHMARK RESULTS ===
|
||||
Test data: <size> MB (<file count> files)
|
||||
Platform: <OS, CPU, network>
|
||||
|
||||
Configuration | Run 1 | Run 2 | Run 3 | Median
|
||||
-----------------------|---------|---------|---------|--------
|
||||
Standard | 0.12s | 0.11s | 0.12s | 0.12s
|
||||
Compression (-c) | 0.09s | 0.08s | 0.09s | 0.09s
|
||||
Multithreading (-m) | 0.07s | 0.07s | 0.08s | 0.07s
|
||||
MT+Compression (-m -c) | 0.05s | 0.05s | 0.06s | 0.05s
|
||||
Sendfile (-f) | 0.04s | 0.04s | 0.04s | 0.04s
|
||||
|
||||
Best configuration: MT+Compression (-m -c)
|
||||
Throughput: <X> MB/s
|
||||
```
|
||||
|
||||
### Step 6: Profiling (If Requested)
|
||||
|
||||
For detailed profiling:
|
||||
```bash
|
||||
# perf
|
||||
perf record -g ./build/client [args...]
|
||||
perf report
|
||||
|
||||
# gprof
|
||||
gcc -pg -o build/client_profile [sources]
|
||||
./build/client_profile [args]
|
||||
gprof build/client_profile gmon.out
|
||||
```
|
||||
|
||||
## Rules
|
||||
- DO build with Release mode for benchmarks
|
||||
- DO run each config multiple times (at least 3)
|
||||
- DO clean destination between runs
|
||||
- DO report median, not just one run
|
||||
- DON'T run benchmarks during active development (noisy results)
|
||||
- ALWAYS clean up test data after benchmarking
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: debug-workflow
|
||||
description: Debugs crashes, memory errors, hangs, and logic bugs in FastSync using structured methodology. Use when the user says "debug X", "fix crash", "investigate failure", "there's a bug", or needs help diagnosing issues.
|
||||
---
|
||||
|
||||
# Debug Workflow Skill
|
||||
|
||||
Structured debugging for FastSync: reproduce → isolate → diagnose → fix → verify. This skill CAN edit files, build, and run tests.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Understand the Problem
|
||||
|
||||
Ask or gather:
|
||||
- What's the symptom? (crash, hang, wrong output, valgrind error)
|
||||
- What command triggers it?
|
||||
- Is it deterministic or intermittent?
|
||||
- What's the environment? (OS, compiler, network conditions)
|
||||
|
||||
### Step 2: Reproduce
|
||||
|
||||
Build with debug info:
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S . -DCMAKE_BUILD_TYPE=Debug
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
Try to reproduce the issue with the exact command the user provides.
|
||||
|
||||
### Step 3: Isolate with Sanitizers
|
||||
|
||||
**Memory errors (first priority):**
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
# or run the failing command
|
||||
```
|
||||
|
||||
**Thread errors:**
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=thread -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
**Valgrind (if ASan doesn't find it):**
|
||||
```bash
|
||||
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \
|
||||
./build/client --source-dir /tmp/src --dest-dir /tmp/dst --save-to-disk
|
||||
```
|
||||
|
||||
### Step 4: GDB Analysis
|
||||
|
||||
If the issue is a crash or hang:
|
||||
```bash
|
||||
gdb --args ./build/client [args...]
|
||||
(gdb) run
|
||||
# when it crashes:
|
||||
(gdb) bt full
|
||||
(gdb) info locals
|
||||
(gdb) print variable_name
|
||||
```
|
||||
|
||||
For hangs:
|
||||
```bash
|
||||
# In another terminal:
|
||||
kill -SIGABRT <pid> # generates core dump
|
||||
gdb ./build/client core
|
||||
(gdb) thread apply all bt
|
||||
```
|
||||
|
||||
### Step 5: Read the Code
|
||||
|
||||
Read the relevant source files around the crash/failure point. Look for:
|
||||
- Unchecked return values
|
||||
- Null pointer dereferences
|
||||
- Buffer overflows
|
||||
- Use-after-free
|
||||
- Race conditions
|
||||
- Incorrect protocol handling
|
||||
|
||||
### Step 6: Diagnose Root Cause
|
||||
|
||||
Identify the exact file:line and what's wrong. Common patterns:
|
||||
- `send_n_data` / `receive_n_data` return value not checked
|
||||
- `data_destroy()` called but pointer still used
|
||||
- Queue operation without mutex in threaded code
|
||||
- Partial read/write not handled
|
||||
- Integer overflow in size calculations
|
||||
|
||||
### Step 7: Fix
|
||||
|
||||
Apply the minimal fix. Don't refactor while debugging — one change at a time.
|
||||
|
||||
### Step 8: Verify
|
||||
|
||||
```bash
|
||||
# Rebuild and test
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
|
||||
# If integration test needed
|
||||
python3 test.py
|
||||
|
||||
# Re-run under sanitizer to confirm fix
|
||||
rm -rf build
|
||||
cmake -B build -S . -DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build -j$(nproc)
|
||||
# reproduce the original failing command
|
||||
```
|
||||
|
||||
### Step 9: Report
|
||||
|
||||
Print a summary:
|
||||
```
|
||||
=== DEBUG SUMMARY ===
|
||||
Symptom: <what was happening>
|
||||
Root cause: <file:line — what's wrong>
|
||||
Fix: <what was changed>
|
||||
Verification: <how it was confirmed fixed>
|
||||
```
|
||||
|
||||
## Rules
|
||||
- DO edit source files to fix issues
|
||||
- DO rebuild and test after fixes
|
||||
- DON'T refactor while debugging — minimal changes only
|
||||
- DON'T change behavior beyond fixing the bug
|
||||
- PRESERVE existing code style
|
||||
- ALWAYS verify with `./build/tests` after changes
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: pr-build
|
||||
description: Builds and tests a pull request branch, fixing compilation errors and test failures. Use when the user says "build PR", "fix PR build", "run PR build", or wants to compile and test a PR branch.
|
||||
---
|
||||
|
||||
# PR Build Skill
|
||||
|
||||
Builds, tests, and fixes a pull request branch. This skill CAN edit files, commit, and push.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Identify the PR branch
|
||||
|
||||
If the user specifies a PR number, check it out:
|
||||
```bash
|
||||
tea pr checkout <number>
|
||||
```
|
||||
|
||||
If already on a PR branch, verify with:
|
||||
```bash
|
||||
git branch --show-current
|
||||
git log main..HEAD --oneline
|
||||
```
|
||||
|
||||
### Step 2: Clean build
|
||||
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S . 2>&1
|
||||
cmake --build build -j$(nproc) 2>&1
|
||||
```
|
||||
|
||||
Capture both stdout and stderr.
|
||||
|
||||
### Step 2b: Sanitizer build (if issues suspected)
|
||||
|
||||
If the PR touches threading, memory management, or network code, also build with sanitizers:
|
||||
|
||||
```bash
|
||||
# AddressSanitizer
|
||||
rm -rf build-asan
|
||||
cmake -B build-asan -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build-asan -j$(nproc)
|
||||
./build-asan/tests
|
||||
|
||||
# ThreadSanitizer (if threading changes)
|
||||
rm -rf build-tsan
|
||||
cmake -B build-tsan -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=thread -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
|
||||
cmake --build build-tsan -j$(nproc)
|
||||
./build-tsan/tests
|
||||
```
|
||||
|
||||
### Step 3: Handle build failures
|
||||
|
||||
If the build fails, read the error output carefully. Common issues:
|
||||
|
||||
**Missing include / undefined reference:**
|
||||
- Check if the new `.c` file is in the right `file(GLOB ...)` directory
|
||||
- Check if the new `.h` file is included properly
|
||||
- Check if CMakeLists.txt needs updating (new target, new source file, new dependency)
|
||||
|
||||
**Type errors / implicit declarations:**
|
||||
- Check function signatures match between `.h` and `.c`
|
||||
- Check struct field names and types
|
||||
|
||||
**Linker errors:**
|
||||
- Check if all required libraries are linked in CMakeLists.txt
|
||||
- Check if all source files are included in the target
|
||||
|
||||
Use the cmake-expert agent to diagnose and fix CMake issues.
|
||||
|
||||
### Step 4: Run unit tests
|
||||
|
||||
If build succeeds:
|
||||
```bash
|
||||
./build/tests
|
||||
```
|
||||
|
||||
### Step 5: Handle test failures
|
||||
|
||||
If tests fail:
|
||||
- Read the test output carefully
|
||||
- Check which test function failed and the assertion line
|
||||
- Read the test source file and the module being tested
|
||||
- Use the test-writer agent to investigate and fix
|
||||
|
||||
### Step 6: Run integration tests (optional)
|
||||
|
||||
```bash
|
||||
python3 test.py
|
||||
```
|
||||
|
||||
This runs the integration + benchmark suite. It takes longer — only run if the user asks or if unit tests pass.
|
||||
|
||||
### Step 7: Fix and commit
|
||||
|
||||
If fixes were needed:
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Fix build: <brief description of what was fixed>"
|
||||
git push
|
||||
```
|
||||
|
||||
### Step 8: Report results
|
||||
|
||||
Print a summary:
|
||||
|
||||
```
|
||||
=== PR BUILD SUMMARY ===
|
||||
Branch: <branch-name>
|
||||
Build: [PASS/FAIL]
|
||||
Unit tests: [PASS/FAIL] (<passed>/<total>)
|
||||
ASan: [CLEAN/ERRORS]
|
||||
TSan: [CLEAN/ERRORS/SKIPPED]
|
||||
Integration tests: [PASS/FAIL/SKIPPED]
|
||||
|
||||
Fixes applied: <count>
|
||||
<list of fixes with commit hashes>
|
||||
```
|
||||
|
||||
## Rules
|
||||
- DO edit source files and CMakeLists.txt to fix issues
|
||||
- DO commit and push fixes
|
||||
- Always build from clean state (rm -rf build)
|
||||
- Read error messages carefully before fixing
|
||||
- Don't change functionality — only fix build/test issues
|
||||
- Preserve existing code style when making fixes
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
name: pr-review
|
||||
description: Reviews a pull request for bugs, memory safety, thread safety, and style issues. Use when the user says "review PR", "review this PR", "review pull request", or wants a code review of changes.
|
||||
---
|
||||
|
||||
# PR Review Skill
|
||||
|
||||
Read-only code review of a pull request branch. Produces a report — does NOT edit files.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Identify the PR branch
|
||||
|
||||
If the user specifies a PR number, check it out:
|
||||
```bash
|
||||
tea pr checkout <number>
|
||||
```
|
||||
|
||||
If already on a PR branch, verify with:
|
||||
```bash
|
||||
git branch --show-current
|
||||
git log main..HEAD --oneline
|
||||
```
|
||||
|
||||
### Step 2: Get changed files
|
||||
|
||||
```bash
|
||||
git diff main --name-only -- '*.c' '*.h'
|
||||
```
|
||||
|
||||
This gives the list of C source and header files changed in the PR.
|
||||
|
||||
### Step 3: Read all changed files
|
||||
|
||||
Use the Read tool to read every changed `.c` and `.h` file. Read full files — don't skip any.
|
||||
|
||||
### Step 4: Review each file
|
||||
|
||||
For each changed file, review for:
|
||||
|
||||
**Memory Safety**
|
||||
- Every `malloc`/`calloc` has a matching `free` on all code paths (including error paths)
|
||||
- No use-after-free (pointers used after `*_destroy()` is called)
|
||||
- No double-free
|
||||
- Null checks after allocation before use
|
||||
- Correct buffer sizes (strlen + 1 for null terminators)
|
||||
- `Data` objects created/destroyed properly
|
||||
|
||||
**Thread Safety**
|
||||
- Shared state accessed under mutex
|
||||
- No race conditions on queue operations
|
||||
- Condition variable signals under lock
|
||||
- No deadlock potential (consistent lock ordering)
|
||||
- `done` flags checked properly in consumer loops
|
||||
|
||||
**Protocol Safety**
|
||||
- `send_n_data` / `receive_n_data` return values checked
|
||||
- Status codes validated before use
|
||||
- Config serialization handles partial reads
|
||||
|
||||
**Logic Errors**
|
||||
- Off-by-one in loops/buffers
|
||||
- Incorrect size calculations
|
||||
- Wrong enum values or comparisons
|
||||
- Missing break statements in switch
|
||||
|
||||
**Error Handling**
|
||||
- Resources freed on error paths (no leaks)
|
||||
- Functions return appropriate error values
|
||||
- Error messages are useful
|
||||
|
||||
**Security**
|
||||
- No `strcpy`/`strcat`/`sprintf` — use `snprintf` with bounds
|
||||
- `malloc` size calculations don't overflow
|
||||
- Path traversal prevention (`..` in filenames)
|
||||
- No fixed-size stack buffers for unbounded input
|
||||
- TLS error codes checked after `SSL_read`/`SSL_write`
|
||||
- No hardcoded certificates, keys, or credentials
|
||||
- Received file permissions validated (no SUID/SGID injection)
|
||||
- Denial of service: bounded memory, malformed messages handled
|
||||
|
||||
**Performance Impact**
|
||||
- Unnecessary memory copies in hot paths
|
||||
- Excessive malloc/free in tight loops
|
||||
- Missing `sendfile()` opportunity for large files
|
||||
- Compression level appropriate for use case
|
||||
- Queue sizing appropriate for workload
|
||||
|
||||
### Step 5: Categorize findings
|
||||
|
||||
For each issue:
|
||||
1. **File:line** — exact location
|
||||
2. **Severity** — critical / warning / style
|
||||
3. **Category** — memory / thread / protocol / security / performance / logic / error
|
||||
4. **Description** — what's wrong and how to fix it
|
||||
|
||||
### Step 6: Output report
|
||||
|
||||
Print a formatted summary:
|
||||
|
||||
```
|
||||
=== PR REVIEW SUMMARY ===
|
||||
Branch: <branch-name>
|
||||
Files reviewed: <count>
|
||||
Issues found: <count>
|
||||
|
||||
CRITICAL: <count>
|
||||
WARNING: <count>
|
||||
STYLE: <count>
|
||||
|
||||
=== ISSUES ===
|
||||
[1] src/shared/compression.c:42 — CRITICAL (memory)
|
||||
Potential leak: data returned from data_compress() not freed on error path
|
||||
Fix: Add data_destroy(compressed) before return false
|
||||
|
||||
...
|
||||
|
||||
=== VERDICT ===
|
||||
[PASS] No critical issues found
|
||||
— or —
|
||||
[FAIL] <N> critical issues must be fixed before merge
|
||||
```
|
||||
|
||||
### Step 7: Optional PR comment
|
||||
|
||||
If the user wants to post the review as a PR comment:
|
||||
```bash
|
||||
tea pr comment <number> --comment "<review report>"
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Do NOT edit any source files
|
||||
- Do NOT run builds or tests
|
||||
- Do NOT commit or push
|
||||
- Report ALL issues — don't filter or minimize
|
||||
- Be specific about line numbers and fix suggestions
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
name: refactor
|
||||
description: Refactors FastSync code for structural improvements — DRY, separation of concerns, API simplification. Use when the user says "refactor X", "clean up code", "improve structure", or wants to reduce duplication.
|
||||
---
|
||||
|
||||
# Refactor Skill
|
||||
|
||||
Read-only analysis + code edits for structural improvements. This skill CAN edit files but MUST verify tests pass.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Identify Refactoring Target
|
||||
|
||||
Ask or determine:
|
||||
- What code needs refactoring?
|
||||
- What's the problem? (duplication, complexity, wrong abstraction, naming)
|
||||
- What's the scope? (single function, module, cross-module)
|
||||
|
||||
### Step 2: Read and Understand
|
||||
|
||||
Read the relevant source files completely. Understand:
|
||||
- What the code does
|
||||
- How it fits in the larger system
|
||||
- What depends on it
|
||||
- What it depends on
|
||||
|
||||
### Step 3: Verify Baseline
|
||||
|
||||
Before any changes, confirm tests pass:
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
### Step 4: Plan the Refactor
|
||||
|
||||
Document the plan:
|
||||
1. What changes will be made
|
||||
2. What behavior is preserved
|
||||
3. What risks exist
|
||||
4. How to verify correctness
|
||||
|
||||
### Step 5: Implement
|
||||
|
||||
Make the changes, one logical step at a time. Follow existing code conventions:
|
||||
- Header guards: `#ifndef FILENAME_H`
|
||||
- Naming: `snake_case` with module prefix
|
||||
- `static` for file-local functions
|
||||
- Pointer style: `Type *name`
|
||||
- Error handling: return `false`/`NULL` on failure
|
||||
|
||||
### Step 6: Build and Test
|
||||
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
ALL tests must pass. If a test fails, investigate and fix.
|
||||
|
||||
### Step 7: Report
|
||||
|
||||
Print a summary:
|
||||
```
|
||||
=== REFACTOR SUMMARY ===
|
||||
Target: <what was refactored>
|
||||
Changes:
|
||||
- <list of changes>
|
||||
Tests: <passed/total>
|
||||
Behavior preserved: yes
|
||||
```
|
||||
|
||||
## Rules
|
||||
- DO edit source files
|
||||
- DO run tests after changes
|
||||
- DO follow existing code conventions
|
||||
- DON'T change observable behavior
|
||||
- DON'T fix bugs while refactoring (separate concern)
|
||||
- DON'T add new features during refactoring
|
||||
- DON'T rewrite from scratch — incremental changes
|
||||
- ALWAYS verify tests pass before AND after
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: release
|
||||
description: Prepares a FastSync release — version bump, changelog, build verification, and git tagging. Use when the user says "prepare release", "bump version", "tag release", or wants to cut a new version.
|
||||
---
|
||||
|
||||
# Release Skill
|
||||
|
||||
Prepares a new release of FastSync. This skill CAN edit files, commit, and tag.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Determine Version
|
||||
|
||||
Ask the user or determine from context:
|
||||
- **Major** (X.0.0) — breaking protocol changes, incompatible CLI changes
|
||||
- **Minor** (x.Y.0) — new features, backward compatible
|
||||
- **Patch** (x.y.Z) — bug fixes, no protocol changes
|
||||
|
||||
Current version: `PROTOCOL_VERSION "1.1.0"` in `src/shared/config.h`
|
||||
|
||||
### Step 2: Check Protocol Version
|
||||
|
||||
If the wire protocol changed, bump `PROTOCOL_VERSION` in `src/shared/config.h`:
|
||||
```c
|
||||
#define PROTOCOL_VERSION "1.2.0" // or "2.0.0" for breaking
|
||||
```
|
||||
|
||||
Protocol version changes require:
|
||||
- Both client and server to be updated together
|
||||
- Backward compatibility considerations documented
|
||||
- Migration path clear
|
||||
|
||||
### Step 3: Verify Build and Tests
|
||||
|
||||
```bash
|
||||
rm -rf build
|
||||
cmake -B build -S .
|
||||
cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
python3 test.py
|
||||
```
|
||||
|
||||
ALL tests must pass before release.
|
||||
|
||||
### Step 4: Run Sanitizer Checks
|
||||
|
||||
```bash
|
||||
# ASan
|
||||
rm -rf build
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
### Step 5: Update README (If Needed)
|
||||
|
||||
Check if README needs updates:
|
||||
- New features documented
|
||||
- New CLI flags documented
|
||||
- Benchmark results updated
|
||||
- Build instructions current
|
||||
|
||||
### Step 6: Create Release Commit
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "Release vX.Y.Z
|
||||
|
||||
- <list of changes>
|
||||
- Protocol version: X.Y.Z
|
||||
- Tested: unit tests, integration tests, ASan"
|
||||
```
|
||||
|
||||
### Step 7: Tag the Release
|
||||
|
||||
```bash
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
||||
```
|
||||
|
||||
### Step 8: Push
|
||||
|
||||
```bash
|
||||
git push origin main --tags
|
||||
```
|
||||
|
||||
### Step 9: Report
|
||||
|
||||
```
|
||||
=== RELEASE SUMMARY ===
|
||||
Version: vX.Y.Z
|
||||
Protocol: X.Y.Z
|
||||
Commit: <hash>
|
||||
Tag: vX.Y.Z
|
||||
Changes:
|
||||
- <list of changes in this release>
|
||||
Build: PASS
|
||||
Tests: PASS (<passed>/<total>)
|
||||
ASan: CLEAN
|
||||
```
|
||||
|
||||
## Rules
|
||||
- DO verify all tests pass before release
|
||||
- DO run sanitizer checks before release
|
||||
- DO update README if features changed
|
||||
- DO tag releases with annotated tags
|
||||
- DON'T release if tests fail
|
||||
- DON'T skip sanitizer checks
|
||||
- DON'T change code during release (only version bump + docs)
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
name: security-audit
|
||||
description: Performs a security audit of FastSync — checks TLS config, input validation, buffer safety, crypto hygiene, and network attack surface. Use when the user says "security audit", "check security", "harden", or wants a security review.
|
||||
---
|
||||
|
||||
# Security Audit Skill
|
||||
|
||||
Read-only security review of the FastSync codebase or specific modules. Produces a report — does NOT edit files.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Scope the Audit
|
||||
|
||||
Determine what to audit:
|
||||
- Full codebase audit
|
||||
- Specific module (e.g., `transport_tls.c`, `protocol.c`)
|
||||
- Specific vulnerability class (e.g., buffer overflows, TLS misconfig)
|
||||
|
||||
### Step 2: Identify Attack Surface
|
||||
|
||||
Network input points:
|
||||
```
|
||||
src/server/server.c — TCP accept, per-connection handling
|
||||
src/shared/protocol.c — all wire protocol parsing
|
||||
src/shared/config.c — config deserialization
|
||||
src/shared/chunk.c — chunk deserialization
|
||||
src/shared/transport_tls.c — TLS handshake and data
|
||||
src/shared/transport_ssh.c — SSH data via stdio
|
||||
```
|
||||
|
||||
### Step 3: Read All Relevant Files
|
||||
|
||||
Read every file in scope completely. Focus on:
|
||||
- All `receive_*` calls and their validation
|
||||
- All `malloc`/`calloc` calls and their size calculations
|
||||
- All string operations (`strcpy`, `sprintf`, `snprintf`)
|
||||
- All path operations (filename handling, directory creation)
|
||||
- All TLS/SSL operations and error handling
|
||||
|
||||
### Step 4: Apply Security Checklist
|
||||
|
||||
#### Input Validation
|
||||
- [ ] All `receive_*` return values checked
|
||||
- [ ] Received size fields validated against bounds
|
||||
- [ ] Path traversal prevention (`..` in filenames)
|
||||
- [ ] Null bytes in filenames handled
|
||||
- [ ] Chunk/file counts validated before allocation
|
||||
|
||||
#### Buffer Safety
|
||||
- [ ] No `strcpy` — use `snprintf`
|
||||
- [ ] `malloc` size calculations don't overflow
|
||||
- [ ] No fixed-size stack buffers for unbounded input
|
||||
- [ ] Off-by-one in path concatenation
|
||||
|
||||
#### TLS/SSL
|
||||
- [ ] TLS 1.2 minimum enforced
|
||||
- [ ] Certificate verification when CA provided
|
||||
- [ ] SSL error codes checked after `SSL_read`/`SSL_write`
|
||||
- [ ] No hardcoded certificates/keys
|
||||
- [ ] Strong cipher suites only
|
||||
|
||||
#### Memory Safety in Error Paths
|
||||
- [ ] All error paths free allocated resources
|
||||
- [ ] No use-after-free on error paths
|
||||
- [ ] Partial reads handled
|
||||
|
||||
#### Denial of Service
|
||||
- [ ] Bounded memory allocation
|
||||
- [ ] Timeout on connections
|
||||
- [ ] Malformed messages handled gracefully
|
||||
|
||||
### Step 5: Check for Common Vulnerabilities
|
||||
|
||||
```bash
|
||||
# Grep for dangerous patterns
|
||||
grep -rn "strcpy\|strcat\|sprintf" src/
|
||||
grep -rn "malloc.*\*" src/ # potential integer overflow in size calc
|
||||
grep -rn "receive_n_data" src/ # check all return values
|
||||
grep -rn "NULL" src/ | grep -v "//" # check null handling
|
||||
```
|
||||
|
||||
### Step 6: Output Report
|
||||
|
||||
```
|
||||
=== SECURITY AUDIT SUMMARY ===
|
||||
Scope: <what was audited>
|
||||
Files reviewed: <count>
|
||||
|
||||
Critical: <count>
|
||||
High: <count>
|
||||
Medium: <count>
|
||||
Low: <count>
|
||||
Informational: <count>
|
||||
|
||||
=== FINDINGS ===
|
||||
|
||||
[1] <file:line> — CRITICAL (<category>)
|
||||
Description: <what's wrong>
|
||||
Exploit scenario: <how it could be triggered>
|
||||
Fix: <concrete code change>
|
||||
|
||||
...
|
||||
|
||||
=== VERDICT ===
|
||||
[PASS] No critical/high issues found
|
||||
— or —
|
||||
[FAIL] <N> critical/high issues must be fixed
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Do NOT edit any source files
|
||||
- Do NOT run builds or tests
|
||||
- Report ALL issues — don't filter or minimize
|
||||
- Be specific about line numbers and fix suggestions
|
||||
- Consider both remote and local attack vectors
|
||||
@@ -0,0 +1,74 @@
|
||||
# AGENTS.md
|
||||
|
||||
FastSync is a high-performance file synchronization system written in C11. It supports TCP and SSH transports, TLS encryption (OpenSSL), streaming zstd compression, multithreaded transfers, and incremental sync. The build uses CMake; CI runs on Gitea Actions (`.gitea/workflows/ci.yaml`).
|
||||
|
||||
## Dependency installation
|
||||
|
||||
**CI rule:** never add `apt-get install` / `pip install` steps to CI workflows — use the custom Docker image instead. The image is built from the repo-root `Dockerfile` and is the same image CI uses: `gitea.tap-tap.win/taptap/fastsync-ci:v7`. It contains the full toolchain: gcc/g++, CMake, libzstd-dev, libssl-dev, make, git, cppcheck, clang-format, python3 + pytest, openssh-client, and Node.js.
|
||||
|
||||
**Host rule:** for local development, use `nix-shell` (see `README.md`) which provides zstd, OpenSSL, CMake, and gcc. The Docker image can also be used locally for CI parity.
|
||||
|
||||
```bash
|
||||
# Use the prebuilt CI image directly (faster, guaranteed CI parity)
|
||||
docker pull gitea.tap-tap.win/taptap/fastsync-ci:v7
|
||||
docker tag gitea.tap-tap.win/taptap/fastsync-ci:v7 fastsync-ci:local
|
||||
|
||||
# Or build the image from the repo-root Dockerfile
|
||||
# (Note: the prebuilt :v7 image reflects the previous Dockerfile state;
|
||||
# rebuild from source to pick up any newly added packages like lcov/valgrind.)
|
||||
docker build -t fastsync-ci:local .
|
||||
|
||||
# Build, run unit tests, and run integration tests inside the container
|
||||
docker run --rm -v "$PWD:/workspace" -w /workspace fastsync-ci:local \
|
||||
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/'
|
||||
|
||||
# Avoid root-owned build/ artifacts by matching your host UID/GID
|
||||
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/workspace" \
|
||||
-w /workspace fastsync-ci:local \
|
||||
sh -c 'cmake -B build -S . && cmake --build build -j$(nproc) && ./build/tests && python3 -m pytest tests/'
|
||||
```
|
||||
|
||||
> **Note:** The first `cmake configure` (`cmake -B build -S .`) fetches xxHash from GitHub via `FetchContent` — network access is required. Subsequent reconfigures reuse the cached source.
|
||||
|
||||
If a dependency is missing from the CI image, add it to the `Dockerfile` (and rebuild) rather than adding an install step to the CI workflow.
|
||||
|
||||
## CI Conventions
|
||||
|
||||
When configuring for CI parity, use:
|
||||
```bash
|
||||
cmake -B build -S . -DSTRICT_WARNINGS=ON # -Wextra -Wpedantic -Werror
|
||||
cmake -B build -S . -DSANITIZER=address # AddressSanitizer (ASan)
|
||||
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (TSan)
|
||||
```
|
||||
|
||||
The CI workflow (`.gitea/workflows/ci.yaml`) runs lint (clang-format, cppcheck), build + test (unit + integration), and sanitizer (currently only `address`) jobs sequentially.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
./build/tests # unit tests
|
||||
python3 -m pytest tests/ # integration tests
|
||||
```
|
||||
|
||||
## CI Workflow — Waiting for Results
|
||||
|
||||
When running the CI workflow via `tea` (the task execution agent), always set a sufficient timeout (e.g., 600000ms) to allow CI to finish. After CI completes, check the results yourself — do not assume success. Use `gh run watch` or similar to monitor CI status, then inspect logs on failure.
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Never push directly to `main`. All changes must be developed on a feature branch and merged via a pull request. Always create a new branch before making changes:
|
||||
```bash
|
||||
git checkout -b <feature-branch-name>
|
||||
```
|
||||
After committing changes, push the branch and create a PR:
|
||||
```bash
|
||||
git push -u origin <feature-branch-name>
|
||||
gh pr create --fill
|
||||
```
|
||||
Wait for CI to pass on the PR before merging.
|
||||
+73
-9
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 4.1)
|
||||
cmake_minimum_required(VERSION 3.22)
|
||||
|
||||
project(FastFileTransfer)
|
||||
|
||||
@@ -7,9 +7,45 @@ set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
add_compile_options(-Wall -g -O3)
|
||||
# add_compile_options(-Wall -g -O1 -fsanitize=address)
|
||||
|
||||
# add_link_options(-fsanitize=address)
|
||||
# --- Sanitizer option ---
|
||||
set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, undefined, none)")
|
||||
set_property(CACHE SANITIZER PROPERTY STRINGS address thread undefined none)
|
||||
|
||||
if(SANITIZER STREQUAL "address")
|
||||
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
|
||||
add_link_options(-fsanitize=address)
|
||||
elseif(SANITIZER STREQUAL "thread")
|
||||
add_compile_options(-fsanitize=thread -fno-omit-frame-pointer -g)
|
||||
add_link_options(-fsanitize=thread)
|
||||
elseif(SANITIZER STREQUAL "undefined")
|
||||
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer -g)
|
||||
add_link_options(-fsanitize=undefined)
|
||||
elseif(NOT SANITIZER STREQUAL "none")
|
||||
message(FATAL_ERROR "Unknown sanitizer: ${SANITIZER}. Supported values: address, thread, undefined, none")
|
||||
endif()
|
||||
|
||||
# --- Strict warnings option ---
|
||||
option(STRICT_WARNINGS "Enable strict warnings (Wextra, Wpedantic, Werror)" OFF)
|
||||
if(STRICT_WARNINGS)
|
||||
add_compile_options(-Wextra -Wpedantic -Werror)
|
||||
endif()
|
||||
|
||||
# --- Coverage option ---
|
||||
option(ENABLE_COVERAGE "Enable gcov coverage" OFF)
|
||||
if(ENABLE_COVERAGE)
|
||||
add_compile_options(--coverage -fprofile-arcs -ftest-coverage -O0 -g)
|
||||
add_link_options(--coverage)
|
||||
endif()
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
xxhash
|
||||
GIT_REPOSITORY https://github.com/Cyan4973/xxHash
|
||||
GIT_TAG v0.8.3
|
||||
SOURCE_SUBDIR cmake_unofficial
|
||||
)
|
||||
FetchContent_MakeAvailable(xxhash)
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
@@ -19,20 +55,48 @@ if(NOT ZSTD_LIBRARY)
|
||||
message(FATAL_ERROR "zstd library not found. Ensure it is in your nix-shell!")
|
||||
endif()
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
file(GLOB SHARED_SRCS "src/shared/*.c")
|
||||
file(GLOB SERVER_SRCS "src/server/*.c")
|
||||
file(GLOB CLIENT_SRCS "src/client/*.c")
|
||||
file(GLOB TEST_SRCS "tests/*.c")
|
||||
|
||||
# --- Main executables ---
|
||||
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS})
|
||||
target_include_directories(server PRIVATE src/shared src/server src/client)
|
||||
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY})
|
||||
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
|
||||
add_executable(client ${CLIENT_SRCS} ${SHARED_SRCS})
|
||||
target_include_directories(client PRIVATE src/shared src/server src/client)
|
||||
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY})
|
||||
target_link_libraries(client PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
|
||||
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS})
|
||||
target_include_directories(tests PRIVATE tests src/shared src/server src/client)
|
||||
target_link_libraries(tests PRIVATE Threads::Threads ${ZSTD_LIBRARY})
|
||||
# --- Testing ---
|
||||
enable_testing()
|
||||
|
||||
# Common test libraries
|
||||
set(TEST_LIBS Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
set(TEST_INCLUDES tests src/shared src/server src/client)
|
||||
|
||||
# Monolithic test binary (backward compatible)
|
||||
file(GLOB TEST_SRCS "tests/test_*.c" "tests/runner.c")
|
||||
add_executable(tests ${TEST_SRCS} ${SHARED_SRCS} src/client/scanner.c)
|
||||
target_include_directories(tests PRIVATE ${TEST_INCLUDES})
|
||||
target_link_libraries(tests PRIVATE ${TEST_LIBS})
|
||||
add_test(NAME unit_all COMMAND tests)
|
||||
|
||||
# --- Fuzz targets (requires clang) ---
|
||||
option(ENABLE_FUZZ "Build fuzz targets (requires clang)" OFF)
|
||||
if(ENABLE_FUZZ)
|
||||
if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||
message(FATAL_ERROR "ENABLE_FUZZ requires Clang (compiler is ${CMAKE_C_COMPILER_ID})")
|
||||
endif()
|
||||
file(GLOB FUZZ_SRCS "tests/fuzz/*.c")
|
||||
foreach(FUZZ_SRC ${FUZZ_SRCS})
|
||||
get_filename_component(FUZZ_NAME ${FUZZ_SRC} NAME_WE)
|
||||
add_executable(${FUZZ_NAME} ${FUZZ_SRC} ${SHARED_SRCS})
|
||||
target_include_directories(${FUZZ_NAME} PRIVATE ${TEST_INCLUDES})
|
||||
target_compile_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined -fno-omit-frame-pointer)
|
||||
target_link_options(${FUZZ_NAME} PRIVATE -fsanitize=fuzzer,address,undefined)
|
||||
target_link_libraries(${FUZZ_NAME} PRIVATE ${TEST_LIBS})
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc g++ make libc6-dev cmake libzstd-dev libssl-dev git ca-certificates curl cppcheck clang-format \
|
||||
python3 python3-pip python3-venv openssl openssh-client \
|
||||
lcov valgrind clang libclang-rt-18-dev && \
|
||||
pip3 install --break-system-packages pytest && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
@@ -0,0 +1,278 @@
|
||||
# FastSync
|
||||
|
||||
A high-performance file synchronization system with SSH and TCP transport, TLS encryption, streaming zstd compression, multithreaded transfer, incremental sync, metadata preservation, and rsync-compatible CLI flags.
|
||||
|
||||
## Technical Overview
|
||||
|
||||
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
|
||||
3. **Chunked file transfer**: files grouped into configurable-size chunks (default ~10 MB)
|
||||
4. **Streaming zstd compression** (levels 1–22) using `ZSTD_compressStream2`
|
||||
5. **Multithreading**: producer-consumer pipeline with thread-safe queues (scanner → loader → sender)
|
||||
6. **Incremental sync**: skip files unchanged since last transfer (compares size + mtime)
|
||||
7. **Metadata preservation**: `mode`, `uid`, `gid`, `mtime` restored on disk when enabled
|
||||
8. **`sendfile()` zero-copy** on TCP (~2× faster on loopback)
|
||||
9. **SSH ControlMaster** for connection reuse across repeated invocations
|
||||
10. **Bandwidth limiting**: token-bucket throttling (`--bwlimit`)
|
||||
11. **`--delete`**: receiver removes files not present in sender manifest
|
||||
12. **`--exclude` / `--include`**: glob-pattern filename filtering
|
||||
|
||||
## System Architecture
|
||||
|
||||
### Client
|
||||
- Recursively scans source directories (BFS), supports exclude and include patterns
|
||||
- Groups files into chunks (configurable size)
|
||||
- Streaming zstd compression with configurable level
|
||||
- Chunk serialization (compact binary format) or per-file transfer
|
||||
- Incremental transfer: sends file metadata to server, skips unchanged files
|
||||
- Manifests all sent paths when `--delete` is active
|
||||
- Sends via TCP `sendfile()` or SSH pipe
|
||||
- Optional progress display with throughput
|
||||
- Bandwidth limiting via token-bucket algorithm
|
||||
|
||||
### Server
|
||||
- TCP mode: listens on configurable port (default 8080); SSH mode: runs via `--stdio`
|
||||
- TLS mode: wraps TCP connections with OpenSSL
|
||||
- Receives and reassembles files
|
||||
- Decompresses (streaming zstd), deserializes, restores metadata
|
||||
- Handles incremental checks: compares size + mtime against destination files
|
||||
- Processes `STATUS_MANIFEST` for `--delete`: walks destination tree, removes extras
|
||||
- Per-connection concurrency via `fork()`
|
||||
- Thread pool for parallel processing
|
||||
|
||||
## Protocol Details
|
||||
|
||||
### Status Codes
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `STATUS_OK` | Operation successful |
|
||||
| `STATUS_ERROR` | Error occurred |
|
||||
| `STATUS_FINISHED` | Transfer complete |
|
||||
| `STATUS_NEXT` | Ready for next file (per-file mode) |
|
||||
| `STATUS_CHUNK` | Following data is a serialized chunk |
|
||||
| `STATUS_MANIFEST` | Following data is a file manifest (for `--delete`) |
|
||||
| `STATUS_CHECK` | Incremental check: client sends file path + size + mtime, server responds with OK (skip) or NEXT (send) |
|
||||
|
||||
### Wire Format — Metadata
|
||||
|
||||
When `use_metadata` is enabled (`-M`), each file entry carries a 4-byte `present` flag followed by five fields (`mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec`). When disabled globally, no metadata bytes are sent — zero wire overhead.
|
||||
|
||||
### Transfer Flow
|
||||
```
|
||||
Config → (STATUS_NEXT | STATUS_CHUNK | STATUS_CHECK)* → [STATUS_MANIFEST] → STATUS_FINISHED → STATUS_OK
|
||||
```
|
||||
|
||||
### Protocol Version
|
||||
|
||||
`1.1.0` — server and client must match. Mismatch results in `STATUS_ERROR`.
|
||||
|
||||
## Command-Line Arguments
|
||||
|
||||
### Client
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| Positional | `<source> <dest>` — automatic SSH detection if dest contains `:` |
|
||||
| `-c [level]` | Compression with optional level (1–22, default 5) |
|
||||
| `-z [level]` | Alias for `-c` |
|
||||
| `-a, --archive` | Archive mode: enables `-c -m -M` (no `-s`) |
|
||||
| `-m` | Multithreading mode |
|
||||
| `-s` | Chunk serialization (batch all files per chunk) |
|
||||
| `-f, --sendfile` | Sendfile zero-copy. Incompatible with `-c` / `-s`. TCP only. |
|
||||
| `-M, --preserve` | Preserve file metadata (mode, uid, gid, mtime) |
|
||||
| `-n, --dry-run` | Scan and print what would be transferred |
|
||||
| `-p <port>` | SSH port (default: 22) |
|
||||
| `-v, --verbose` | Enable debug logging |
|
||||
| `--progress` | Show real-time transfer speed |
|
||||
| `--delete` | Delete files on receiver not present in source |
|
||||
| `--exclude <pattern>` | Exclude files matching glob pattern (repeatable) |
|
||||
| `--include <pattern>` | Only transfer files matching glob pattern (repeatable, whitelist) |
|
||||
| `--max-size <n>` | Skip files larger than n bytes |
|
||||
| `--min-size <n>` | Skip files smaller than n bytes |
|
||||
| `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `-s`. |
|
||||
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
||||
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
||||
| `--source-dir <path>` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) |
|
||||
| `--dest-dir <path>` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) |
|
||||
| `--save-to-disk` | Write received files to disk |
|
||||
| `--server-host <ip>` | Server IP address (default: `127.0.0.1`) |
|
||||
| `--server-port <n>` | Server port (default: `8080`) |
|
||||
| `--tls` | Enable TLS encryption |
|
||||
| `--cert <path>` | TLS certificate file (PEM) |
|
||||
| `--key <path>` | TLS private key file (PEM) |
|
||||
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
||||
|
||||
### Server
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| `--stdio` | Run in stdio mode (for SSH transport; single connection then exits) |
|
||||
| `-p <port>` | TCP listen port (default: 8080, range: 1–65535) |
|
||||
| `--tls` | Enable TLS encryption |
|
||||
| `--cert <path>` | TLS certificate file (PEM) |
|
||||
| `--key <path>` | TLS private key file (PEM) |
|
||||
| `--ca <path>` | TLS CA certificate file for verification (PEM) |
|
||||
| `-v, --verbose` | Enable debug logging |
|
||||
| `--help` | Show help |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `FASTSYNC_SOURCE_DIR` | — | Source directory fallback |
|
||||
| `FASTSYNC_DEST_DIR` | — | Destination directory fallback |
|
||||
| `FASTSYNC_SAVE_TO_DISK` | `false` | Disk persistence fallback |
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Data Structures
|
||||
1. **Chunk** — collection of files (~10 MB total by default)
|
||||
2. **File** — path, content (`Data`), optional `FileMetadata` pointer
|
||||
3. **FileMetadata** — `mode`, `uid`, `gid`, `mtime_sec`, `mtime_nsec`
|
||||
4. **Config** — runtime parameters (transported over wire, TLS settings excluded)
|
||||
5. **Queue** — thread-safe bounded queue with condition variables
|
||||
6. **DirectoryScanner** — recursive BFS traversal with exclude and include pattern support
|
||||
|
||||
### Key Algorithms
|
||||
1. **File scanning** — BFS directory traversal; entries matched against exclude and include patterns
|
||||
2. **Chunking** — files accumulated until `chunk_size` threshold, then flushed
|
||||
3. **Compression** — streaming zstd via `ZSTD_compressStream2` / `ZSTD_decompressStream`
|
||||
4. **Network protocol** — status-code-driven exchange with metadata packing
|
||||
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
|
||||
7. **Metadata restoration** — `chmod()`, `chown()`, `utimensat()` on the receiving side
|
||||
8. **`--delete`** — sender tracks all sent paths; receiver walks destination tree and removes unlisted files/directories
|
||||
9. **SSH transport** — `socketpair()` + `fork()` + `execvp("ssh", ...)` with `ControlMaster` and port support
|
||||
10. **TLS transport** — OpenSSL `SSL_CTX` with TLS 1.2 minimum, optional CA verification, transparent `SSL_read`/`SSL_write` via `io_set_ssl()`
|
||||
|
||||
## Build Requirements
|
||||
|
||||
- C11 compiler
|
||||
- CMake >= 3.22
|
||||
- zstd library
|
||||
- OpenSSL (development headers and libraries)
|
||||
- pthreads
|
||||
- SSH client (for SSH transport mode only)
|
||||
|
||||
### Installing Dependencies
|
||||
|
||||
**Ubuntu/Debian:**
|
||||
```bash
|
||||
sudo apt install cmake build-essential libzstd-dev libssl-dev openssh-client
|
||||
```
|
||||
|
||||
**Nix:**
|
||||
```bash
|
||||
nix-shell # provides zstd, openssl, cmake, gcc
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cmake -B build -S . && cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
### Server (TCP mode)
|
||||
```bash
|
||||
./build/server
|
||||
```
|
||||
|
||||
### Server with TLS
|
||||
```bash
|
||||
./build/server --tls --cert server.pem --key server-key.pem
|
||||
```
|
||||
|
||||
### Server via SSH
|
||||
Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh user@host fastsync-server --stdio` automatically when an SSH-style destination is given.
|
||||
|
||||
### Client — SSH (rsync-style)
|
||||
```bash
|
||||
./build/client /path/to/send user@host:/path/to/receive
|
||||
```
|
||||
|
||||
### Client — TCP
|
||||
```bash
|
||||
./build/client --source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk
|
||||
```
|
||||
|
||||
### Client — TCP with TLS
|
||||
```bash
|
||||
./build/client --tls --cert client.pem --key client-key.pem --ca ca.pem \
|
||||
--source-dir /path/to/send --dest-dir /path/to/receive --save-to-disk
|
||||
```
|
||||
|
||||
### Common Options
|
||||
```bash
|
||||
# Archive mode (compression + multithreading + metadata)
|
||||
./build/client -a /path/to/send user@host:/path
|
||||
|
||||
# Dry run
|
||||
./build/client -n /path/to/send /path/to/receive
|
||||
|
||||
# With progress and custom chunk size
|
||||
./build/client --progress --chunk-size 2097152 /src user@host:/dst
|
||||
|
||||
# Exclude temporary files + delete extras on receiver
|
||||
./build/client --exclude "*.tmp" --exclude "*.o" --delete /src user@host:/dst
|
||||
|
||||
# Incremental sync (skip unchanged files)
|
||||
./build/client --incremental /src user@host:/dst
|
||||
|
||||
# Bandwidth limit to 1 MB/s
|
||||
./build/client --bwlimit 1024 /src user@host:/dst
|
||||
|
||||
# All features
|
||||
./build/client -a --progress --chunk-size 5242880 --exclude "*.log" --delete /src /dst
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Unit tests (7 suites)
|
||||
./build/tests
|
||||
|
||||
# Integration + benchmark suite
|
||||
python3 test.py
|
||||
```
|
||||
|
||||
The benchmark prints throughput metrics, best configuration, and speedup vs rsync.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. Chunk size (~10 MB default) balances memory and transfer efficiency
|
||||
2. Compression level trades CPU for bandwidth
|
||||
3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files
|
||||
4. Multithreading scales with core count
|
||||
5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled)
|
||||
6. SSH socketpair buffer set to 1 MB for improved pipe throughput
|
||||
7. SSH ControlMaster reuses connections across repeated invocations
|
||||
8. Incremental sync eliminates redundant transfers entirely
|
||||
9. Bandwidth limiting uses token-bucket with nanosleep for accurate throttling
|
||||
|
||||
## Benchmark Results
|
||||
|
||||
25 MB of mixed file sizes over `localhost` with disk I/O throttled (reads ≤ 15 MB/s, writes ≤ 10 MB/s) and network emulation via `tc netem`. Each test was run 3×; the median is reported below.
|
||||
|
||||
### LAN (1000 Mbit, 20 ms ±1 ms, 0.1% loss)
|
||||
|
||||
| Configuration | Time | vs rsync (archive) | vs rsync (compress) |
|
||||
|---|---|---|---|
|
||||
| **Best: `-m -c`** | **0.20 s** | **11.2× faster** | **3.6× faster** |
|
||||
| Compression (`-c`) | 0.31 s | 7.3× faster | 2.3× faster |
|
||||
| Standard | 1.27 s | 1.8× faster | — |
|
||||
| rsync (archive) | 2.27 s | — | — |
|
||||
| rsync (archive + compress) | 0.72 s | — | — |
|
||||
|
||||
### WAN (100 Mbit, 50 ms ±10 ms, 1% loss)
|
||||
|
||||
| Configuration | Time | vs rsync (archive) | vs rsync (compress) |
|
||||
|---|---|---|---|
|
||||
| **Best: `-m -c`** | **0.39 s** | **44.8× faster** | **3.8× faster** |
|
||||
| Compression (`-c`) | 0.64 s | 27.3× faster | 2.3× faster |
|
||||
| Standard | 7.12 s | 2.4× faster | — |
|
||||
| rsync (archive) | 17.44 s | — | — |
|
||||
| rsync (archive + compress) | 1.47 s | — | — |
|
||||
|
||||
Compression reduces the data on the wire enough that the transfer becomes latency-bound rather than bandwidth-bound. On WAN, the best configuration runs 10.8× faster than the theoretical limit for uncompressed data, since zstd shrinks the 25 MB payload to a fraction of its original size over the wire.
|
||||
@@ -0,0 +1,519 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Standalone benchmark tool for FastSync.
|
||||
|
||||
Compares FastSync configs against rsync (no compression) and rsync+zstd.
|
||||
Data is ~75% random/incompressible and ~25% structured/compressible by default,
|
||||
controllable via --random-ratio.
|
||||
|
||||
Usage:
|
||||
python3 benchmark/bench.py
|
||||
python3 benchmark/bench.py --runs 5 --profiles lan wan
|
||||
python3 benchmark/bench.py --random-ratio 0.5 --size-mb 50
|
||||
python3 benchmark/bench.py --delay 50ms --jitter 10ms --throughput 100mbit
|
||||
python3 benchmark/bench.py --output json
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
BUILD_DIR = os.path.join(PROJECT_ROOT, "build")
|
||||
SERVER_CMD = [os.path.join(BUILD_DIR, "server")]
|
||||
CLIENT_CMD = [os.path.join(BUILD_DIR, "client")]
|
||||
BENCH_DIR = os.path.join(PROJECT_ROOT, "bench_data")
|
||||
|
||||
NETWORK_PROFILES = {
|
||||
"unlimited": {},
|
||||
"lan": {
|
||||
"rate": "1000mbit", "delay": "20ms", "jitter": "1ms", "loss": "0.1%",
|
||||
"rate_bps": 1_000_000_000 / 8,
|
||||
},
|
||||
"wan": {
|
||||
"rate": "100mbit", "delay": "50ms", "jitter": "10ms", "loss": "1%",
|
||||
"rate_bps": 100_000_000 / 8,
|
||||
},
|
||||
}
|
||||
|
||||
FASTSYNC_CONFIGS = [
|
||||
{"name": "fastsync", "flags": [], "tool": "fastsync"},
|
||||
{"name": "fastsync -c", "flags": ["-c"], "tool": "fastsync"},
|
||||
{"name": "fastsync -m", "flags": ["-m"], "tool": "fastsync"},
|
||||
{"name": "fastsync -m -c", "flags": ["-m", "-c"], "tool": "fastsync"},
|
||||
{"name": "fastsync -m -c -s", "flags": ["-m", "-c", "-s"], "tool": "fastsync"},
|
||||
]
|
||||
|
||||
RSYNC_CONFIGS = [
|
||||
{"name": "rsync", "flags": [], "tool": "rsync"},
|
||||
{"name": "rsync -z", "flags": ["-z"], "tool": "rsync"},
|
||||
{"name": "rsync -z --zstd", "flags": ["-z", "--zc", "zstd"],"tool": "rsync"},
|
||||
]
|
||||
|
||||
class RsyncDaemon:
|
||||
"""Manages an rsync daemon for network-fair benchmarking."""
|
||||
|
||||
def __init__(self):
|
||||
self._proc = None
|
||||
self._port = None
|
||||
self._conf_dir = None
|
||||
self._module_path = None
|
||||
|
||||
def start(self, source_dir):
|
||||
self._port = find_free_port()
|
||||
self._conf_dir = tempfile.mkdtemp(prefix="rsyncd_")
|
||||
self._module_path = source_dir
|
||||
|
||||
conf_path = os.path.join(self._conf_dir, "rsyncd.conf")
|
||||
log_path = os.path.join(self._conf_dir, "rsyncd.log")
|
||||
|
||||
with open(conf_path, "w") as f:
|
||||
f.write(f"uid = 0\ngid = 0\nuse chroot = no\nlog file = {log_path}\n")
|
||||
f.write(f"[bench]\n\tpath = {source_dir}\n\tread only = yes\n")
|
||||
|
||||
self._proc = subprocess.Popen(
|
||||
["rsync", "--daemon", "--no-detach",
|
||||
"--port", str(self._port),
|
||||
"--config", conf_path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
wait_for_port(self._port, timeout=5)
|
||||
|
||||
def stop(self):
|
||||
if self._proc:
|
||||
self._proc.terminate()
|
||||
try:
|
||||
self._proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._proc.kill()
|
||||
self._proc.wait()
|
||||
self._proc = None
|
||||
if self._conf_dir:
|
||||
shutil.rmtree(self._conf_dir, ignore_errors=True)
|
||||
self._conf_dir = None
|
||||
|
||||
@property
|
||||
def source_url(self):
|
||||
return f"rsync://127.0.0.1:{self._port}/bench/"
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.stop()
|
||||
|
||||
|
||||
STRUCTURED_FILES = {
|
||||
"small.txt": b"hello world\n",
|
||||
"medium.txt": b"the quick brown fox jumps over the lazy dog\n" * 5000,
|
||||
"binary.bin": bytes(range(256)) * 1000,
|
||||
"nested/subdir/deep.txt": b"deeply nested file\n",
|
||||
"nested/another.txt": b"another nested file\n" * 50,
|
||||
}
|
||||
|
||||
|
||||
class Progress:
|
||||
"""Simple progress bar with ETA."""
|
||||
|
||||
def __init__(self, total, label="Progress"):
|
||||
self.total = total
|
||||
self.current = 0
|
||||
self.label = label
|
||||
self.start_time = time.monotonic()
|
||||
self._print()
|
||||
|
||||
def tick(self, detail=""):
|
||||
self.current += 1
|
||||
self._print(detail)
|
||||
|
||||
def _print(self, detail=""):
|
||||
elapsed = time.monotonic() - self.start_time
|
||||
if self.current > 0:
|
||||
eta = elapsed / self.current * (self.total - self.current)
|
||||
eta_str = f"ETA {eta:.0f}s"
|
||||
else:
|
||||
eta_str = "ETA ..."
|
||||
pct = self.current / self.total * 100 if self.total else 0
|
||||
bar_len = 30
|
||||
filled = int(bar_len * self.current / self.total) if self.total else 0
|
||||
bar = "#" * filled + "-" * (bar_len - filled)
|
||||
detail_str = f" {detail}" if detail else ""
|
||||
sys.stderr.write(f"\r [{bar}] {pct:5.1f}% {self.current}/{self.total} {eta_str}{detail_str} ")
|
||||
sys.stderr.flush()
|
||||
if self.current >= self.total:
|
||||
sys.stderr.write(f"\r [{'#' * bar_len}] 100.0% {self.total}/{self.total} done in {elapsed:.1f}s" + " " * 30 + "\n")
|
||||
sys.stderr.flush()
|
||||
|
||||
|
||||
def generate_bench_data(source_dir, size_mb=25, random_ratio=0.75):
|
||||
"""Generate test data. ~random_ratio is incompressible, rest is structured."""
|
||||
if os.path.exists(source_dir):
|
||||
shutil.rmtree(source_dir)
|
||||
os.makedirs(source_dir)
|
||||
|
||||
target = size_mb * 1024 * 1024
|
||||
structured_budget = int(target * (1 - random_ratio))
|
||||
written = 0
|
||||
|
||||
for rel_path, content in STRUCTURED_FILES.items():
|
||||
if written >= structured_budget:
|
||||
break
|
||||
full_path = os.path.join(source_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "wb") as f:
|
||||
f.write(content)
|
||||
written += len(content)
|
||||
|
||||
os.makedirs(os.path.join(source_dir, "bulk"), exist_ok=True)
|
||||
i = 0
|
||||
while written < target:
|
||||
chunk_size = min(5 * 1024 * 1024, target - written)
|
||||
with open(os.path.join(source_dir, f"bulk/file_{i}.dat"), "wb") as f:
|
||||
f.write(random.randbytes(chunk_size))
|
||||
written += chunk_size
|
||||
i += 1
|
||||
|
||||
return written
|
||||
|
||||
|
||||
def find_free_port():
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def wait_for_port(port, timeout=5):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.3):
|
||||
return
|
||||
except (ConnectionRefusedError, OSError):
|
||||
time.sleep(0.05)
|
||||
raise RuntimeError(f"Port {port} not ready")
|
||||
|
||||
|
||||
def wait_proc(proc, timeout=5):
|
||||
try:
|
||||
proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
def netem_apply(delay=None, jitter=None, throughput=None, loss=None):
|
||||
"""Apply tc/netem rules to loopback. Pass None to skip a parameter."""
|
||||
netem_reset()
|
||||
cmd = ["sudo", "tc", "qdisc", "add", "dev", "lo", "root", "netem"]
|
||||
if throughput:
|
||||
cmd += ["rate", throughput]
|
||||
if delay:
|
||||
cmd += ["delay", delay, jitter or "0ms"]
|
||||
if loss:
|
||||
cmd += ["loss", loss]
|
||||
if len(cmd) > 6:
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
|
||||
|
||||
def netem_apply_profile(profile_name):
|
||||
params = NETWORK_PROFILES.get(profile_name, {})
|
||||
if not params:
|
||||
netem_reset()
|
||||
return
|
||||
netem_apply(
|
||||
delay=params.get("delay"),
|
||||
jitter=params.get("jitter"),
|
||||
throughput=params.get("rate"),
|
||||
loss=params.get("loss"),
|
||||
)
|
||||
|
||||
|
||||
def netem_reset():
|
||||
subprocess.run("sudo tc qdisc del dev lo root".split(), capture_output=True)
|
||||
|
||||
|
||||
def run_fastsync(source_dir, dest_dir, flags, port):
|
||||
"""Run FastSync client. Returns duration or None."""
|
||||
cmd = CLIENT_CMD + [
|
||||
"--source-dir", source_dir,
|
||||
"--dest-dir", dest_dir,
|
||||
"--server-port", str(port),
|
||||
"--save-to-disk",
|
||||
] + flags
|
||||
try:
|
||||
start = time.monotonic()
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
duration = time.monotonic() - start
|
||||
if result.returncode == 0:
|
||||
return duration
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def run_rsync(source_dir, dest_dir, flags, rsync_daemon=None):
|
||||
"""Run rsync. Returns duration or None."""
|
||||
src = source_dir.rstrip("/") + "/"
|
||||
if rsync_daemon:
|
||||
src = rsync_daemon.source_url
|
||||
cmd = ["rsync", "-a", "--delete"] + flags + [src, dest_dir + "/"]
|
||||
try:
|
||||
start = time.monotonic()
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
duration = time.monotonic() - start
|
||||
if result.returncode == 0:
|
||||
return duration
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def run_transfer(config, source_dir, dest_dir, port=None, rsync_daemon=None):
|
||||
"""Route to the right tool. Returns duration or None."""
|
||||
if config["tool"] == "rsync":
|
||||
return run_rsync(source_dir, dest_dir, config["flags"], rsync_daemon)
|
||||
else:
|
||||
return run_fastsync(source_dir, dest_dir, config["flags"], port)
|
||||
|
||||
|
||||
def run_benchmark(source_dir, dest_dir, configs, runs, profile_name, progress=None):
|
||||
"""Run benchmark for all configs, returns list of results."""
|
||||
is_limited = profile_name != "unlimited"
|
||||
has_rsync = any(c["tool"] == "rsync" for c in configs)
|
||||
if is_limited:
|
||||
netem_apply_profile(profile_name)
|
||||
|
||||
rsync_daemon = None
|
||||
try:
|
||||
if is_limited and has_rsync:
|
||||
rsync_daemon = RsyncDaemon()
|
||||
rsync_daemon.start(source_dir)
|
||||
|
||||
results = []
|
||||
for config in configs:
|
||||
times = []
|
||||
for run_idx in range(runs):
|
||||
if os.path.exists(dest_dir):
|
||||
shutil.rmtree(dest_dir)
|
||||
os.makedirs(dest_dir, exist_ok=True)
|
||||
|
||||
port = find_free_port()
|
||||
server = None
|
||||
try:
|
||||
if config["tool"] == "fastsync":
|
||||
server = subprocess.Popen(
|
||||
SERVER_CMD + ["-p", str(port)],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
)
|
||||
wait_for_port(port)
|
||||
|
||||
t = run_transfer(config, source_dir, dest_dir, port, rsync_daemon)
|
||||
if t is not None:
|
||||
times.append(t)
|
||||
finally:
|
||||
if server:
|
||||
wait_proc(server)
|
||||
|
||||
if progress:
|
||||
progress.tick(f"{config['name']} (run {run_idx+1}/{runs})")
|
||||
|
||||
entry = {
|
||||
"config": config["name"],
|
||||
"tool": config["tool"],
|
||||
"profile": profile_name,
|
||||
"runs": len(times),
|
||||
"times": [round(t, 4) for t in times],
|
||||
}
|
||||
if times:
|
||||
entry["p50"] = round(statistics.median(times), 4)
|
||||
entry["p95"] = round(sorted(times)[int(len(times) * 0.95)], 4) if len(times) > 1 else entry["p50"]
|
||||
entry["min"] = round(min(times), 4)
|
||||
entry["max"] = round(max(times), 4)
|
||||
entry["stdev"] = round(statistics.stdev(times), 4) if len(times) > 1 else 0.0
|
||||
results.append(entry)
|
||||
return results
|
||||
finally:
|
||||
if rsync_daemon:
|
||||
rsync_daemon.stop()
|
||||
if is_limited:
|
||||
netem_reset()
|
||||
|
||||
|
||||
def print_table(results, total_bytes, random_ratio):
|
||||
"""Print results as a human-readable table grouped by profile."""
|
||||
profiles = {}
|
||||
for r in results:
|
||||
profiles.setdefault(r["profile"], []).append(r)
|
||||
|
||||
for profile, entries in profiles.items():
|
||||
params = NETWORK_PROFILES.get(profile, {})
|
||||
print(f"\n{'=' * 85}")
|
||||
print(f" Profile: {profile.upper()}")
|
||||
if params.get("rate"):
|
||||
print(f" Network: {params['rate']}, {params['delay']} +/- {params['jitter']}, loss {params['loss']}")
|
||||
else:
|
||||
print(f" Network: unlimited")
|
||||
print(f" Data: {total_bytes / (1024*1024):.1f} MB ({random_ratio*100:.0f}% random, {(1-random_ratio)*100:.0f}% compressible)")
|
||||
print(f"{'=' * 85}")
|
||||
|
||||
fs_entries = [e for e in entries if e.get("tool") == "fastsync"]
|
||||
rsync_entries = [e for e in entries if e.get("tool") == "rsync"]
|
||||
|
||||
if fs_entries:
|
||||
print(f"\n FastSync:")
|
||||
print(f" {'Config':<25} {'p50':>8} {'p95':>8} {'min':>8} {'max':>8} {'stdev':>8} {'runs':>5}")
|
||||
print(f" {'-' * 25} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 5}")
|
||||
for e in sorted(fs_entries, key=lambda x: x.get("p50", 999)):
|
||||
_print_entry(e)
|
||||
|
||||
if rsync_entries:
|
||||
print(f"\n rsync:")
|
||||
print(f" {'Config':<25} {'p50':>8} {'p95':>8} {'min':>8} {'max':>8} {'stdev':>8} {'runs':>5}")
|
||||
print(f" {'-' * 25} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 8} {'-' * 5}")
|
||||
for e in sorted(rsync_entries, key=lambda x: x.get("p50", 999)):
|
||||
_print_entry(e)
|
||||
|
||||
if params.get("rate_bps") and fs_entries and rsync_entries:
|
||||
fs_best = min((e["p50"] for e in fs_entries if "p50" in e), default=None)
|
||||
rsync_best = min((e["p50"] for e in rsync_entries if "p50" in e), default=None)
|
||||
theoretical = total_bytes / params["rate_bps"]
|
||||
if fs_best and rsync_best:
|
||||
print(f"\n Theoretical max (line rate): {theoretical:.4f}s")
|
||||
print(f" FastSync best: {fs_best:.4f}s ({theoretical/fs_best:.2f}x vs line rate)")
|
||||
print(f" rsync best: {rsync_best:.4f}s ({theoretical/rsync_best:.2f}x vs line rate)")
|
||||
print(f" FastSync vs rsync: {rsync_best/fs_best:.2f}x faster")
|
||||
|
||||
|
||||
def _print_entry(e):
|
||||
if "p50" in e:
|
||||
print(f" {e['config']:<25} {e['p50']:>7.4f}s {e['p95']:>7.4f}s "
|
||||
f"{e['min']:>7.4f}s {e['max']:>7.4f}s {e['stdev']:>7.4f} {e['runs']:>5}")
|
||||
else:
|
||||
print(f" {e['config']:<25} {'N/A':>8} {'N/A':>8} {'N/A':>8} {'N/A':>8} {'N/A':>8} {e['runs']:>5}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="FastSync benchmark tool — compares FastSync vs rsync",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""\
|
||||
Network profiles (predefined):
|
||||
unlimited No artificial limits
|
||||
lan 1 Gbit, 20ms delay, 1ms jitter, 0.1%% loss
|
||||
wan 100 Mbit, 50ms delay, 10ms jitter, 1%% loss
|
||||
|
||||
Custom network limits (--delay/--jitter/--throughput) override profiles.
|
||||
|
||||
Data mix:
|
||||
Default is ~75%% random/incompressible + ~25%% structured/compressible,
|
||||
reflecting typical real-world file sets.
|
||||
|
||||
Examples:
|
||||
%(prog)s --profiles wan --runs 5
|
||||
%(prog)s --throughput 50mbit --delay 30ms --jitter 5ms
|
||||
%(prog)s --random-ratio 0.5 --size-mb 100
|
||||
""")
|
||||
parser.add_argument("--runs", type=int, default=3,
|
||||
help="Number of runs per config (default: 3)")
|
||||
parser.add_argument("--profiles", nargs="+", default=None,
|
||||
choices=list(NETWORK_PROFILES.keys()),
|
||||
help="Predefined network profiles (default: unlimited)")
|
||||
parser.add_argument("--configs", nargs="+", default=None,
|
||||
help="Custom FastSync config flags")
|
||||
parser.add_argument("--size-mb", type=int, default=25,
|
||||
help="Test data size in MB (default: 25)")
|
||||
parser.add_argument("--random-ratio", type=float, default=0.75,
|
||||
help="Fraction of data that is random/incompressible (default: 0.75)")
|
||||
parser.add_argument("--delay", default=None,
|
||||
help="Custom network delay (e.g. 50ms)")
|
||||
parser.add_argument("--jitter", default=None,
|
||||
help="Custom network jitter (e.g. 10ms)")
|
||||
parser.add_argument("--throughput", default=None,
|
||||
help="Custom throughput limit (e.g. 100mbit)")
|
||||
parser.add_argument("--loss", default=None,
|
||||
help="Custom packet loss (e.g. 1%%)")
|
||||
parser.add_argument("--no-rsync", action="store_true",
|
||||
help="Skip rsync comparison")
|
||||
parser.add_argument("--progress", action="store_true",
|
||||
help="Show progress bar with ETA")
|
||||
parser.add_argument("--output", choices=["table", "json"], default="table",
|
||||
help="Output format")
|
||||
parser.add_argument("--keep-data", action="store_true",
|
||||
help="Don't clean up test data")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Build
|
||||
print("Building...")
|
||||
if os.system(f"cmake -B {BUILD_DIR} -S {PROJECT_ROOT} > /dev/null 2>&1") != 0:
|
||||
print("CMake configure failed"); sys.exit(1)
|
||||
if os.system(f"cmake --build {BUILD_DIR} -j$(nproc) > /dev/null 2>&1") != 0:
|
||||
print("Build failed"); sys.exit(1)
|
||||
|
||||
# Determine active profile for display
|
||||
has_custom_net = args.delay or args.jitter or args.throughput or args.loss
|
||||
if has_custom_net:
|
||||
active_profile = "custom"
|
||||
NETWORK_PROFILES["custom"] = {
|
||||
"rate": args.throughput, "delay": args.delay or "0ms",
|
||||
"jitter": args.jitter or "0ms", "loss": args.loss or "0%",
|
||||
}
|
||||
if args.throughput:
|
||||
parts = args.throughput.replace("mbit", "").replace("mbps", "")
|
||||
try:
|
||||
NETWORK_PROFILES["custom"]["rate_bps"] = float(parts) * 1_000_000 / 8
|
||||
except ValueError:
|
||||
pass
|
||||
profiles_to_run = ["custom"]
|
||||
else:
|
||||
profiles_to_run = args.profiles or ["unlimited"]
|
||||
|
||||
# Generate data
|
||||
source_dir = os.path.join(BENCH_DIR, "source")
|
||||
dest_dir = os.path.join(BENCH_DIR, "dest")
|
||||
total_bytes = generate_bench_data(source_dir, args.size_mb, args.random_ratio)
|
||||
compressible_pct = (1 - args.random_ratio) * 100
|
||||
random_pct = args.random_ratio * 100
|
||||
print(f"Generated {total_bytes / (1024*1024):.1f} MB "
|
||||
f"({random_pct:.0f}% random, {compressible_pct:.0f}% compressible)")
|
||||
|
||||
# Build config list
|
||||
if args.configs:
|
||||
fastsync_configs = [{"name": c, "flags": c.split(), "tool": "fastsync"} for c in args.configs]
|
||||
else:
|
||||
fastsync_configs = list(FASTSYNC_CONFIGS)
|
||||
|
||||
configs = list(fastsync_configs)
|
||||
if not args.no_rsync:
|
||||
configs += RSYNC_CONFIGS
|
||||
|
||||
# Run benchmarks
|
||||
total_runs = len(configs) * args.runs * len(profiles_to_run)
|
||||
progress = Progress(total_runs, "Benchmarking") if args.progress else None
|
||||
if progress:
|
||||
print(f"Running {total_runs} transfers...")
|
||||
|
||||
all_results = []
|
||||
try:
|
||||
for profile in profiles_to_run:
|
||||
results = run_benchmark(source_dir, dest_dir, configs, args.runs, profile, progress)
|
||||
all_results.extend(results)
|
||||
finally:
|
||||
if not args.keep_data:
|
||||
shutil.rmtree(BENCH_DIR, ignore_errors=True)
|
||||
|
||||
# Output
|
||||
if args.output == "json":
|
||||
print(json.dumps(all_results, indent=2))
|
||||
else:
|
||||
print_table(all_results, total_bytes, args.random_ratio)
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,24 +0,0 @@
|
||||
# jetstream - A High-Performance File Transfer Utility
|
||||
|
||||
## Features
|
||||
|
||||
## Architecture ideas
|
||||
|
||||
### Pipeline Stages Client
|
||||
|
||||
- Directory Scanning -> Files
|
||||
- File Reading -> SendableBuffer
|
||||
- optional: Compression -> Sendable Buffer
|
||||
- Send Data
|
||||
|
||||
### Pipeline Stages Server
|
||||
|
||||
- Receive Data -> Sendable Buffer
|
||||
- optional: Decompress -> Files
|
||||
- File Writing
|
||||
|
||||
### Passing Data between Steps
|
||||
|
||||
- use Queue
|
||||
|
||||
##
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"instructions": ["AGENTS.md"],
|
||||
"permission": {
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"git push origin main": "deny",
|
||||
"git push main": "deny"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,14 @@ pkgs.mkShell {
|
||||
cmake
|
||||
gnumake
|
||||
pkg-config
|
||||
docker
|
||||
tea
|
||||
];
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
zstd
|
||||
openssl
|
||||
(python3.withPackages (ps: with ps; [ pytest ]))
|
||||
];
|
||||
|
||||
NIX_ENFORCE_PURITY = 0;
|
||||
@@ -19,5 +23,6 @@ pkgs.mkShell {
|
||||
shellHook = ''
|
||||
export NIX_ENFORCE_PURITY=0
|
||||
cmake -B build
|
||||
export PATH="$PWD/build:$PATH"
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <threads.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "chunk.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "multiprocessing.h"
|
||||
#include "queue.h"
|
||||
#include "scanner.h"
|
||||
#include "socket.h"
|
||||
#include "utils.h"
|
||||
#include <dirent.h>
|
||||
|
||||
int send_chunk(Client *client, Chunk *chunk, Config *config) {
|
||||
if (config->use_compression && config->use_chunk_serialization) {
|
||||
Data *data = chunk_compress(chunk, config->compression_level);
|
||||
send_data(client->file_descriptor, data->data, data->size);
|
||||
} else {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
send_status(client->file_descriptor, STATUS_NEXT);
|
||||
File *file = chunk->items[i];
|
||||
if (config->use_compression) {
|
||||
Data *compressed_data =
|
||||
data_compress(file->data, config->compression_level);
|
||||
data_destroy(file->data);
|
||||
file->data = compressed_data;
|
||||
}
|
||||
file_send_single_calls(file, client->file_descriptor);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int scan_directory_multithreaded(void *pipeline_context) {
|
||||
PipelineContextSender *context = (PipelineContextSender *)pipeline_context;
|
||||
mtx_lock(&context->mutex_scanner);
|
||||
DirectoryScanner *scanner =
|
||||
directory_scanner_create(context->config->send_directory);
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
|
||||
Chunk *current_chunk;
|
||||
while ((current_chunk = directory_scanner_next(scanner)) != NULL)
|
||||
queue_enqueue_multithreaded(context->queue_scanner, current_chunk,
|
||||
&context->mutex_scanner,
|
||||
&context->condition_not_empty_scanner,
|
||||
&context->condition_not_full_scanner);
|
||||
mtx_lock(&context->mutex_scanner);
|
||||
context->scanner_done = true;
|
||||
cnd_signal(&context->condition_not_empty_scanner);
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
int load_files_multithreaded(void *pipeline_context) {
|
||||
PipelineContextSender *context = (PipelineContextSender *)pipeline_context;
|
||||
while (true) {
|
||||
Chunk *chunk = queue_dequeue_multithreaded(
|
||||
context->queue_scanner, &context->mutex_scanner,
|
||||
&context->condition_not_empty_scanner,
|
||||
&context->condition_not_full_scanner, &context->scanner_done);
|
||||
if (chunk == NULL) {
|
||||
mtx_lock(&context->mutex_loader);
|
||||
context->loader_done = true;
|
||||
cnd_signal(&context->condition_not_empty_loader);
|
||||
mtx_unlock(&context->mutex_loader);
|
||||
return thrd_success;
|
||||
}
|
||||
for (int i = 0; i < chunk->element_count; i++)
|
||||
file_load_data(chunk->items[i]);
|
||||
queue_enqueue_multithreaded(context->queue_loader, chunk,
|
||||
&context->mutex_loader,
|
||||
&context->condition_not_empty_loader,
|
||||
&context->condition_not_full_loader);
|
||||
}
|
||||
}
|
||||
|
||||
int send_chunks_multithreaded(void *pipeline_context) {
|
||||
PipelineContextSender *context = (PipelineContextSender *)pipeline_context;
|
||||
Client *client = client_create();
|
||||
const char *env_ip = getenv("FASTSYNC_SERVER_IP");
|
||||
const char *ip = env_ip ? env_ip : "127.0.0.1";
|
||||
const char *env_port = getenv("FASTSYNC_SERVER_PORT");
|
||||
int port = env_port ? atoi(env_port) : 8080;
|
||||
client_connect(client, (char *)ip, port);
|
||||
config_send(client->file_descriptor, context->config);
|
||||
|
||||
while (true) {
|
||||
Chunk *current_chunk = queue_dequeue_multithreaded(
|
||||
context->queue_loader, &context->mutex_loader,
|
||||
&context->condition_not_empty_loader,
|
||||
&context->condition_not_full_loader, &context->loader_done);
|
||||
if (current_chunk == NULL) {
|
||||
send_status(client->file_descriptor, STATUS_FINISHED);
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return thrd_success;
|
||||
}
|
||||
if (send_chunk(client, current_chunk, context->config) != 0) {
|
||||
perror("Something unexpected happend while sending the chunk");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
chunk_destroy(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
int send_files(Config *config) {
|
||||
Client *client = client_create();
|
||||
const char *env_ip = getenv("FASTSYNC_SERVER_IP");
|
||||
const char *ip = env_ip ? env_ip : "127.0.0.1";
|
||||
const char *env_port = getenv("FASTSYNC_SERVER_PORT");
|
||||
int port = env_port ? atoi(env_port) : 8080;
|
||||
client_connect(client, (char *)ip, port);
|
||||
config_send(client->file_descriptor, config);
|
||||
DirectoryScanner *scanner = directory_scanner_create(config->send_directory);
|
||||
Chunk *current_chunk;
|
||||
while ((current_chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
for (int i = 0; i < current_chunk->element_count; i++)
|
||||
file_load_data(current_chunk->items[i]);
|
||||
send_chunk(client, current_chunk, config);
|
||||
chunk_destroy(current_chunk);
|
||||
}
|
||||
send_status(client->file_descriptor, STATUS_FINISHED);
|
||||
if (receive_status(client->file_descriptor) != STATUS_OK)
|
||||
return -1;
|
||||
directory_scanner_destroy(scanner);
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_files_multithreaded(Config *config) {
|
||||
PipelineContextSender *context =
|
||||
pipeline_context_sender_create(config, queue_create(100, chunk_destroy),
|
||||
queue_create(100, chunk_destroy));
|
||||
|
||||
thrd_t scanner, loader, sender;
|
||||
if (thrd_create(&scanner, scan_directory_multithreaded, context) !=
|
||||
thrd_success ||
|
||||
thrd_create(&loader, load_files_multithreaded, context) != thrd_success ||
|
||||
thrd_create(&sender, send_chunks_multithreaded, context) !=
|
||||
thrd_success) {
|
||||
perror("Error creating threads.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
thrd_join(scanner, NULL);
|
||||
thrd_join(loader, NULL);
|
||||
thrd_join(sender, NULL);
|
||||
|
||||
pipeline_context_sender_destroy(context);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void handle_arg(char *argument_given, char *argument_to_set, bool *result,
|
||||
char *message) {
|
||||
if (strcmp(argument_given, argument_to_set) == 0) {
|
||||
*result = true;
|
||||
log_message(LOG_LEVEL_INFO, message);
|
||||
}
|
||||
}
|
||||
int main(int argc, char *argv[]) {
|
||||
const char *env_source = getenv("FASTSYNC_SOURCE_DIR");
|
||||
const char *env_dest = getenv("FASTSYNC_DEST_DIR");
|
||||
const char *env_save = getenv("FASTSYNC_SAVE_TO_DISK");
|
||||
|
||||
char *source_dir =
|
||||
env_source ? str_dup((char *)env_source)
|
||||
: str_dup("/home/taptap/Nextcloud/Uni/moodle/B. Schnor: "
|
||||
"Konzepte Paralleler Programmierung, SoSe 2026");
|
||||
char *dest_dir =
|
||||
env_dest ? str_dup((char *)env_dest) : str_dup("./data_copied");
|
||||
bool save_to_disk = false;
|
||||
if (env_save &&
|
||||
(strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) {
|
||||
save_to_disk = true;
|
||||
}
|
||||
|
||||
Config *config = config_create(str_dup("1.0.0"), source_dir, dest_dir,
|
||||
save_to_disk, false, false, false, 5, 20);
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-c") == 0) {
|
||||
config->use_compression = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled Compression");
|
||||
|
||||
if (i + 1 < argc) {
|
||||
char *end_ptr;
|
||||
int level = strtol(argv[i + 1], &end_ptr, 10);
|
||||
if (*end_ptr == '\0') {
|
||||
config->compression_level = level;
|
||||
log_message(LOG_LEVEL_INFO, "Set Compression level to %d",
|
||||
config->compression_level);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handle_arg(argv[i], "-m", &config->use_multithreading,
|
||||
"Enabled Multithreading");
|
||||
handle_arg(argv[i], "-s", &config->use_chunk_serialization,
|
||||
"Enabled Chunk Serialization");
|
||||
}
|
||||
}
|
||||
|
||||
if (config->use_multithreading)
|
||||
return send_files_multithreaded(config);
|
||||
return send_files(config);
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
#include "client_send.h"
|
||||
#include "config.h"
|
||||
#include "delta.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "transport_tls.h"
|
||||
#include "utils.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void print_usage(void) {
|
||||
printf("Usage:\n");
|
||||
printf(" fastsync [options] <source> <destination>\n");
|
||||
printf(" fastsync [options] --source-dir <src> --dest-dir <dst>\n");
|
||||
printf("\n");
|
||||
printf("Destination formats:\n");
|
||||
printf(" user@host:/path SSH transport (rsync-style)\n");
|
||||
printf(" host:/path SSH transport (current user)\n");
|
||||
printf(" /local/path TCP transport (requires server on localhost:8080)\n");
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" -c [level] Enable compression (level 1-22, default 5)\n");
|
||||
printf(" -z [level] Alias for -c\n");
|
||||
printf(" -a, --archive Archive mode (-c -m -M)\n");
|
||||
printf(" -n, --dry-run Show what would be transferred\n");
|
||||
printf(" -p <port> SSH port (default: 22)\n");
|
||||
printf(" --progress Show transfer progress\n");
|
||||
printf(" --delete Delete files on receiver not in source\n");
|
||||
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
||||
printf(" --include <pattern> Only include files matching pattern\n");
|
||||
printf(" --max-size <n> Skip files larger than n bytes\n");
|
||||
printf(" --min-size <n> Skip files smaller than n bytes\n");
|
||||
printf(" --incremental Skip files unchanged since last transfer\n");
|
||||
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
|
||||
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
|
||||
DELTA_BLOCK_SIZE_DEFAULT);
|
||||
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
|
||||
DELTA_MAX_FILE_SIZE);
|
||||
printf(" -m Enable multithreading\n");
|
||||
printf(" -s Enable chunk serialization\n");
|
||||
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
|
||||
printf(" -v, --verbose Enable debug logging\n");
|
||||
printf(" -M, --preserve Preserve file metadata\n");
|
||||
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
|
||||
printf(" --source-dir <path> Source directory\n");
|
||||
printf(" --dest-dir <path> Destination directory\n");
|
||||
printf(" --save-to-disk Write received files to disk\n");
|
||||
printf(" --server-host <ip> Server IP address (default: 127.0.0.1)\n");
|
||||
printf(" --server-port <n> Server port (default: 8080)\n");
|
||||
printf(" --bwlimit <KB/s> Bandwidth limit in kilobytes per second\n");
|
||||
printf(" --tls Enable TLS encryption\n");
|
||||
printf(" --cert <path> TLS certificate file (PEM)\n");
|
||||
printf(" --key <path> TLS private key file (PEM)\n");
|
||||
printf(" --ca <path> TLS CA certificate file (PEM)\n");
|
||||
printf(" --partial Keep partial files on interrupted transfer\n");
|
||||
printf(" --help Show this help\n");
|
||||
printf(" -V, --version Show version and exit\n");
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
const char* env_source = getenv("FASTSYNC_SOURCE_DIR");
|
||||
const char* env_dest = getenv("FASTSYNC_DEST_DIR");
|
||||
const char* env_save = getenv("FASTSYNC_SAVE_TO_DISK");
|
||||
|
||||
bool save_to_disk = false;
|
||||
if (env_save && (strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) {
|
||||
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_count = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
print_usage();
|
||||
goto cleanup;
|
||||
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
|
||||
printf("fastsync version %s\n", PROTOCOL_VERSION);
|
||||
goto cleanup;
|
||||
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
|
||||
config->use_compression = true;
|
||||
config->use_multithreading = true;
|
||||
config->use_metadata = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled archive mode (-c -m -M)");
|
||||
} else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) {
|
||||
config->dry_run = true;
|
||||
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||
char* end;
|
||||
long p = strtol(argv[++i], &end, 10);
|
||||
if (*end != '\0' || p <= 0 || p > 65535) {
|
||||
fprintf(stderr, "Error: invalid SSH port '%s' (must be 1-65535)\n", argv[i]);
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
config->ssh_port = (int)p;
|
||||
} else if (strcmp(argv[i], "--delete") == 0) {
|
||||
config->use_delete = true;
|
||||
} else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {
|
||||
char** tmp = realloc(config->exclude_patterns, (config->exclude_count + 1) * sizeof(char*));
|
||||
if (!tmp) {
|
||||
fprintf(stderr, "Error: memory allocation failed for --exclude\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
config->exclude_patterns = tmp;
|
||||
config->exclude_patterns[config->exclude_count++] = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--include") == 0 && i + 1 < argc) {
|
||||
char** tmp = realloc(config->include_patterns, (config->include_count + 1) * sizeof(char*));
|
||||
if (!tmp) {
|
||||
fprintf(stderr, "Error: memory allocation failed for --include\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
config->include_patterns = tmp;
|
||||
config->include_patterns[config->include_count++] = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--max-size") == 0 && i + 1 < argc) {
|
||||
config->max_size = strtoull(argv[++i], NULL, 10);
|
||||
} else if (strcmp(argv[i], "--min-size") == 0 && i + 1 < argc) {
|
||||
config->min_size = strtoull(argv[++i], NULL, 10);
|
||||
} else if (strcmp(argv[i], "--incremental") == 0) {
|
||||
config->use_incremental = true;
|
||||
} else if (strcmp(argv[i], "--delta") == 0) {
|
||||
config->use_delta = true;
|
||||
} else if (strcmp(argv[i], "--delta-block") == 0 && i + 1 < argc) {
|
||||
unsigned long long val = strtoull(argv[++i], NULL, 10);
|
||||
if (val >= DELTA_BLOCK_SIZE_MIN && val <= DELTA_BLOCK_SIZE_MAX)
|
||||
config->delta_block_size = (uint32_t)val;
|
||||
else
|
||||
fprintf(stderr, "Warning: --delta-block value %llu out of range, using default\n", val);
|
||||
} else if (strcmp(argv[i], "--delta-max") == 0 && i + 1 < argc) {
|
||||
unsigned long long val = strtoull(argv[++i], NULL, 10);
|
||||
if (val >= DELTA_MIN_FILE_SIZE)
|
||||
config->delta_max_file_size = val;
|
||||
else
|
||||
fprintf(stderr, "Warning: --delta-max value %llu too small, using default\n", val);
|
||||
} else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "-z") == 0) {
|
||||
config->use_compression = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled Compression");
|
||||
if (i + 1 < argc) {
|
||||
char* end_ptr;
|
||||
int level = strtol(argv[i + 1], &end_ptr, 10);
|
||||
if (*end_ptr == '\0') {
|
||||
config->compression_level = level;
|
||||
log_message(LOG_LEVEL_INFO, "Set Compression level to %d", config->compression_level);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else if (strcmp(argv[i], "--source-dir") == 0 && i + 1 < argc) {
|
||||
free(config->send_directory);
|
||||
config->send_directory = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--dest-dir") == 0 && i + 1 < argc) {
|
||||
free(config->receive_root_directory);
|
||||
config->receive_root_directory = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--save-to-disk") == 0) {
|
||||
config->save_to_disk = true;
|
||||
} else if (strcmp(argv[i], "-M") == 0 || strcmp(argv[i], "--preserve") == 0) {
|
||||
config->use_metadata = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled metadata preservation");
|
||||
} else if (strcmp(argv[i], "-f") == 0 || strcmp(argv[i], "--sendfile") == 0) {
|
||||
config->use_sendfile = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled sendfile");
|
||||
} else if (strcmp(argv[i], "-m") == 0) {
|
||||
config->use_multithreading = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled Multithreading");
|
||||
} else if (strcmp(argv[i], "-s") == 0) {
|
||||
config->use_chunk_serialization = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization");
|
||||
} else if (strcmp(argv[i], "--server-host") == 0 && i + 1 < argc) {
|
||||
free(config->server_host);
|
||||
config->server_host = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--server-port") == 0 && i + 1 < argc) {
|
||||
char* end;
|
||||
long p = strtol(argv[++i], &end, 10);
|
||||
if (*end != '\0' || p <= 0 || p > 65535) {
|
||||
fprintf(stderr, "Error: invalid server port '%s' (must be 1-65535)\n", argv[i]);
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
config->server_port = (int)p;
|
||||
} else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) {
|
||||
char* end;
|
||||
errno = 0;
|
||||
unsigned long long kbps = strtoull(argv[++i], &end, 10);
|
||||
if (errno != 0 || *end != '\0' || kbps == 0) {
|
||||
fprintf(stderr, "Error: --bwlimit must be a positive integer\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
if (kbps > ULLONG_MAX / 1024) {
|
||||
fprintf(stderr, "Error: --bwlimit value too large\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
io_set_bwlimit(kbps * 1024);
|
||||
log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps);
|
||||
} else if (strcmp(argv[i], "--progress") == 0) {
|
||||
config->show_progress = true;
|
||||
} else if (strcmp(argv[i], "--chunk-size") == 0 && i + 1 < argc) {
|
||||
unsigned long long val = strtoull(argv[++i], NULL, 10);
|
||||
if (val > 0)
|
||||
config->chunk_size = val;
|
||||
} else if (strcmp(argv[i], "--tls") == 0) {
|
||||
config->use_tls = true;
|
||||
} else if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) {
|
||||
free(config->tls_cert);
|
||||
config->tls_cert = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) {
|
||||
free(config->tls_key);
|
||||
config->tls_key = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
|
||||
free(config->tls_ca);
|
||||
config->tls_ca = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--partial") == 0) {
|
||||
config->partial = true;
|
||||
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||
set_log_level(LOG_LEVEL_DEBUG);
|
||||
} else if (argv[i][0] == '-') {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
print_usage();
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
} else {
|
||||
if (positional_count < 2)
|
||||
positional_args[positional_count++] = i;
|
||||
else {
|
||||
fprintf(stderr, "Unexpected argument: %s\n", argv[i]);
|
||||
print_usage();
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (positional_count == 2) {
|
||||
free(config->send_directory);
|
||||
free(config->receive_root_directory);
|
||||
config->send_directory = str_dup(argv[positional_args[0]]);
|
||||
config->receive_root_directory = str_dup(argv[positional_args[1]]);
|
||||
config->save_to_disk = true;
|
||||
|
||||
config_parse_ssh_dest(config);
|
||||
} else if (positional_count == 1) {
|
||||
fprintf(stderr, "Error: missing destination argument\n");
|
||||
print_usage();
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
} else {
|
||||
if (!config->send_directory && env_source)
|
||||
config->send_directory = str_dup((char*)env_source);
|
||||
if (!config->receive_root_directory && env_dest)
|
||||
config->receive_root_directory = str_dup((char*)env_dest);
|
||||
}
|
||||
|
||||
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;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
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) {
|
||||
config_owned_by_pipeline = true;
|
||||
exit_code = send_files_multithreaded(config);
|
||||
} else {
|
||||
exit_code = send_files(config);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (!config_owned_by_pipeline)
|
||||
config_delete(config);
|
||||
return exit_code;
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
#include "client_send.h"
|
||||
#include "array_list.h"
|
||||
#include "chunk.h"
|
||||
#include "compression.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "delta.h"
|
||||
#include "file.h"
|
||||
#include "metadata.h"
|
||||
#include "log.h"
|
||||
#include "multiprocessing.h"
|
||||
#include "protocol.h"
|
||||
#include "queue.h"
|
||||
#include "scanner.h"
|
||||
#include "transport_tcp.h"
|
||||
#include "transport_ssh.h"
|
||||
#include "transport_tls.h"
|
||||
#include "utils.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <threads.h>
|
||||
#include <time.h>
|
||||
|
||||
static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) {
|
||||
*out_sig = NULL;
|
||||
if (!send_status(client->file_descriptor, STATUS_CHECK))
|
||||
return -1;
|
||||
if (!send_str(client->file_descriptor, file->path))
|
||||
return -1;
|
||||
unsigned long long fsize = file->data->size;
|
||||
long long mtime = file->metadata ? file->metadata->mtime_sec : 0;
|
||||
if (!send_n_data(client->file_descriptor, &fsize, sizeof(fsize)))
|
||||
return -1;
|
||||
if (!send_n_data(client->file_descriptor, &mtime, sizeof(mtime)))
|
||||
return -1;
|
||||
Status s;
|
||||
if (!receive_status(client->file_descriptor, &s))
|
||||
return -1;
|
||||
if (s == STATUS_ERROR) {
|
||||
log_message(LOG_LEVEL_ERROR, "Server reported error for file");
|
||||
return -1;
|
||||
}
|
||||
if (s == STATUS_OK)
|
||||
return 1;
|
||||
if (s == STATUS_DELTA_SIGNATURE) {
|
||||
Data* sig_data = receive_data(client->file_descriptor);
|
||||
if (!sig_data)
|
||||
return -1;
|
||||
DeltaSignature* sig = delta_signature_deserialize(sig_data);
|
||||
data_destroy(sig_data);
|
||||
if (!sig)
|
||||
return -1;
|
||||
*out_sig = sig;
|
||||
return 2;
|
||||
}
|
||||
if (s != STATUS_NEXT) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unexpected server status");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* config) {
|
||||
Delta* delta = delta_compute(file->data->data, file->data->size, sig, config->delta_block_size);
|
||||
if (!delta)
|
||||
return 1;
|
||||
|
||||
if (!delta_is_worthwhile(delta, file->data->size)) {
|
||||
delta_destroy(delta);
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT))
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Data* delta_data = delta_serialize(delta);
|
||||
delta_destroy(delta);
|
||||
if (!delta_data)
|
||||
return -1;
|
||||
|
||||
Data* to_send = delta_data;
|
||||
if (config->use_compression) {
|
||||
to_send = data_compress(delta_data, config->compression_level);
|
||||
data_destroy(delta_data);
|
||||
if (!to_send)
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool ok = send_status(client->file_descriptor, STATUS_DELTA_DATA) &&
|
||||
send_data(client->file_descriptor, to_send);
|
||||
|
||||
if (ok && config->use_metadata)
|
||||
ok = metadata_send(client->file_descriptor, file->metadata);
|
||||
|
||||
data_destroy(to_send);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
typedef bool (*file_send_fn)(File*, int, bool, int, bool);
|
||||
|
||||
// Send a single file directly (non-incremental path).
|
||||
static bool send_file_direct(File* file, int fd, bool use_metadata, int compression_level) {
|
||||
if (!send_status(fd, STATUS_NEXT))
|
||||
return false;
|
||||
return file_send_single_calls(file, fd, use_metadata, compression_level, true);
|
||||
}
|
||||
|
||||
// Send a single file directly via sendfile (non-incremental path).
|
||||
static bool send_file_direct_sendfile(File* file, int fd, bool use_metadata) {
|
||||
if (!send_status(fd, STATUS_NEXT))
|
||||
return false;
|
||||
return file_send_sendfile(file, fd, use_metadata, 0, true);
|
||||
}
|
||||
|
||||
// Process one file in a chunk: either via incremental check or direct send.
|
||||
// Returns 0 on success, 1 if skipped (incremental match), -1 on error.
|
||||
static int send_single_file(Client* client, File* file, Config* config, bool use_incremental,
|
||||
bool use_sendfile) {
|
||||
int compression_level = config->use_compression ? config->compression_level : 0;
|
||||
|
||||
if (!use_incremental) {
|
||||
if (use_sendfile) {
|
||||
return send_file_direct_sendfile(file, client->file_descriptor, config->use_metadata) ? 0
|
||||
: -1;
|
||||
}
|
||||
return send_file_direct(file, client->file_descriptor, config->use_metadata, compression_level)
|
||||
? 0
|
||||
: -1;
|
||||
}
|
||||
|
||||
// Incremental path: use sendfile for the actual data if enabled and no compression
|
||||
if (use_sendfile) {
|
||||
DeltaSignature* sig = NULL;
|
||||
int rc = incremental_check(client, file, &sig);
|
||||
if (rc == 1) {
|
||||
delta_signature_destroy(sig);
|
||||
return 1;
|
||||
}
|
||||
if (rc < 0) {
|
||||
delta_signature_destroy(sig);
|
||||
return -1;
|
||||
}
|
||||
// rc == 0: unchanged file, skip
|
||||
// rc == 2: server sent delta signature but sendfile doesn't support delta
|
||||
delta_signature_destroy(sig);
|
||||
if (rc == 2) {
|
||||
// Server is waiting for STATUS_NEXT after delta handshake
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT))
|
||||
return -1;
|
||||
}
|
||||
// Fall through: send full file via sendfile (pass 0 for compression_level)
|
||||
if (!file_send_sendfile(file, client->file_descriptor, config->use_metadata, 0, false))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Incremental path with single_calls (supports compression and delta)
|
||||
file_send_fn send_fn = (file_send_fn)file_send_single_calls;
|
||||
DeltaSignature* sig = NULL;
|
||||
int rc = incremental_check(client, file, &sig);
|
||||
if (rc < 0) {
|
||||
delta_signature_destroy(sig);
|
||||
return -1;
|
||||
}
|
||||
if (rc == 1) {
|
||||
delta_signature_destroy(sig);
|
||||
return 1;
|
||||
}
|
||||
if (rc == 2 && config->use_delta) {
|
||||
int drc = send_delta(client, file, sig, config);
|
||||
delta_signature_destroy(sig);
|
||||
if (drc == 0)
|
||||
return 0;
|
||||
if (drc < 0)
|
||||
return -1;
|
||||
} else {
|
||||
delta_signature_destroy(sig);
|
||||
// rc == 2 can happen if server sends STATUS_DELTA_SIGNATURE but
|
||||
// use_delta is false on the client side. Send STATUS_NEXT to
|
||||
// tell the server to proceed with the full file transfer.
|
||||
if (rc == 2) {
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT))
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (!send_fn(file, client->file_descriptor, config->use_metadata, compression_level, false))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_chunk(Client* client, Chunk* chunk, Config* config) {
|
||||
if (config->use_chunk_serialization) {
|
||||
if (!send_status(client->file_descriptor, STATUS_CHUNK))
|
||||
return -1;
|
||||
Data* data;
|
||||
if (config->use_compression) {
|
||||
data = chunk_compress(chunk, config->compression_level, config->use_metadata);
|
||||
} else {
|
||||
data = chunk_serialize(chunk, config->use_metadata);
|
||||
}
|
||||
if (data == NULL)
|
||||
return -1;
|
||||
if (!send_data(client->file_descriptor, data)) {
|
||||
data_destroy(data);
|
||||
return -1;
|
||||
}
|
||||
data_destroy(data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool use_sendfile = config->use_sendfile && !config->use_compression;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
int rc =
|
||||
send_single_file(client, chunk->items[i], config, config->use_incremental, use_sendfile);
|
||||
if (rc == 1)
|
||||
continue;
|
||||
if (rc < 0)
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
Client* client;
|
||||
if (context->config->transport == TRANSPORT_SSH) {
|
||||
if (context->config->use_sendfile) {
|
||||
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
|
||||
return 1;
|
||||
}
|
||||
client = client_connect_ssh(context->config->ssh_destination, context->config->ssh_port);
|
||||
} else if (context->config->use_tls) {
|
||||
client = client_create();
|
||||
if (!client || !client_connect_tls(client, context->config->server_host,
|
||||
context->config->server_port, context->config->tls_cert,
|
||||
context->config->tls_key, context->config->tls_ca)) {
|
||||
if (client)
|
||||
client_delete(client);
|
||||
fprintf(stderr, "Error: could not connect to server via TLS\n");
|
||||
return thrd_error;
|
||||
}
|
||||
} else {
|
||||
client = client_create();
|
||||
if (!client ||
|
||||
!client_connect(client, context->config->server_host, context->config->server_port)) {
|
||||
if (client)
|
||||
client_delete(client);
|
||||
fprintf(stderr, "Error: could not connect to server\n");
|
||||
return thrd_error;
|
||||
}
|
||||
}
|
||||
if (!config_send(client->file_descriptor, context->config)) {
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return thrd_error;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Chunk* current_chunk = queue_dequeue_multithreaded(
|
||||
context->queue_loader, &context->mutex_loader, &context->condition_not_empty_loader,
|
||||
&context->condition_not_full_loader, &context->loader_done);
|
||||
if (current_chunk == NULL) {
|
||||
if (context->config->use_delete) {
|
||||
if (!send_status(client->file_descriptor, STATUS_MANIFEST))
|
||||
goto send_fail;
|
||||
if (!send_int(client->file_descriptor, context->manifest->size))
|
||||
goto send_fail;
|
||||
for (int i = 0; i < context->manifest->size; i++) {
|
||||
if (!send_str(client->file_descriptor, (char*)context->manifest->items[i]))
|
||||
goto send_fail;
|
||||
}
|
||||
}
|
||||
if (!send_status(client->file_descriptor, STATUS_FINISHED))
|
||||
goto send_fail;
|
||||
Status s;
|
||||
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK;
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return ok ? thrd_success : thrd_error;
|
||||
|
||||
send_fail:
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return thrd_error;
|
||||
}
|
||||
if (send_chunk(client, current_chunk, context->config) != 0) {
|
||||
fprintf(stderr, "Error: unexpected error while sending chunk\n");
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return thrd_error;
|
||||
}
|
||||
chunk_destroy(current_chunk);
|
||||
}
|
||||
}
|
||||
|
||||
static int scan_directory_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
mtx_lock(&context->mutex_scanner);
|
||||
DirectoryScanner* scanner = directory_scanner_create(
|
||||
context->config->send_directory, context->config->use_metadata, context->config->chunk_size,
|
||||
context->config->exclude_patterns, context->config->exclude_count,
|
||||
context->config->include_patterns, context->config->include_count, context->config->max_size,
|
||||
context->config->min_size);
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
|
||||
Chunk* current_chunk;
|
||||
while ((current_chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
if (context->config->use_delete) {
|
||||
mtx_lock(&context->mutex_scanner);
|
||||
for (int i = 0; i < current_chunk->element_count; i++) {
|
||||
const char* p = current_chunk->items[i]->path;
|
||||
if (*p == '/')
|
||||
p++;
|
||||
array_list_add(context->manifest, str_dup(p));
|
||||
}
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
}
|
||||
queue_enqueue_multithreaded(context->queue_scanner, current_chunk, &context->mutex_scanner,
|
||||
&context->condition_not_empty_scanner,
|
||||
&context->condition_not_full_scanner);
|
||||
}
|
||||
mtx_lock(&context->mutex_scanner);
|
||||
context->scanner_done = true;
|
||||
cnd_signal(&context->condition_not_empty_scanner);
|
||||
mtx_unlock(&context->mutex_scanner);
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
static int load_files_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
while (true) {
|
||||
Chunk* chunk = queue_dequeue_multithreaded(
|
||||
context->queue_scanner, &context->mutex_scanner, &context->condition_not_empty_scanner,
|
||||
&context->condition_not_full_scanner, &context->scanner_done);
|
||||
if (chunk == NULL) {
|
||||
mtx_lock(&context->mutex_loader);
|
||||
context->loader_done = true;
|
||||
cnd_signal(&context->condition_not_empty_loader);
|
||||
mtx_unlock(&context->mutex_loader);
|
||||
return thrd_success;
|
||||
}
|
||||
if (!context->config->use_sendfile) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (!file_load_data(chunk->items[i])) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to load file data, skipping");
|
||||
file_destroy(chunk->items[i]);
|
||||
chunk->items[i] = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
queue_enqueue_multithreaded(context->queue_loader, chunk, &context->mutex_loader,
|
||||
&context->condition_not_empty_loader,
|
||||
&context->condition_not_full_loader);
|
||||
}
|
||||
}
|
||||
|
||||
int send_files(Config* config) {
|
||||
if (config->dry_run) {
|
||||
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;
|
||||
}
|
||||
|
||||
Client* client;
|
||||
if (config->transport == TRANSPORT_SSH) {
|
||||
if (config->use_sendfile) {
|
||||
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
|
||||
return 1;
|
||||
}
|
||||
client = client_connect_ssh(config->ssh_destination, config->ssh_port);
|
||||
if (!client)
|
||||
return 1;
|
||||
} else if (config->use_tls) {
|
||||
client = client_create();
|
||||
if (!client || !client_connect_tls(client, config->server_host, config->server_port,
|
||||
config->tls_cert, config->tls_key, config->tls_ca)) {
|
||||
if (client)
|
||||
client_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(
|
||||
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* current_chunk;
|
||||
unsigned long long total_bytes = 0;
|
||||
time_t last_progress = 0;
|
||||
time_t start = time(NULL);
|
||||
ArrayList* manifest = config->use_delete ? array_list_create(free) : NULL;
|
||||
while ((current_chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
unsigned long long chunk_bytes = 0;
|
||||
for (int i = 0; i < current_chunk->element_count; i++) {
|
||||
chunk_bytes += current_chunk->items[i]->data->size;
|
||||
if (manifest) {
|
||||
const char* p = current_chunk->items[i]->path;
|
||||
if (*p == '/')
|
||||
p++;
|
||||
array_list_add(manifest, str_dup(p));
|
||||
}
|
||||
}
|
||||
if (!config->use_sendfile) {
|
||||
for (int i = 0; i < current_chunk->element_count; i++) {
|
||||
if (!file_load_data(current_chunk->items[i])) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to load file data");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (send_chunk(client, current_chunk, config) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to send chunk");
|
||||
chunk_destroy(current_chunk);
|
||||
break;
|
||||
}
|
||||
if (config->show_progress) {
|
||||
total_bytes += chunk_bytes;
|
||||
time_t now = time(NULL);
|
||||
if (now - last_progress >= 1) {
|
||||
last_progress = now;
|
||||
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);
|
||||
}
|
||||
if (config->use_delete) {
|
||||
if (!send_status(client->file_descriptor, STATUS_MANIFEST)) {
|
||||
array_list_delete(manifest);
|
||||
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);
|
||||
}
|
||||
if (!send_status(client->file_descriptor, STATUS_FINISHED))
|
||||
goto send_fail;
|
||||
Status s;
|
||||
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK;
|
||||
if (config->show_progress) {
|
||||
double elapsed = difftime(time(NULL), start);
|
||||
double rate = elapsed > 0 ? total_bytes / (1048576.0 * elapsed) : 0;
|
||||
fprintf(stderr, "\rSent %.1f MB (%.1f MB/s) Done.\n", total_bytes / 1048576.0, rate);
|
||||
}
|
||||
directory_scanner_destroy(scanner);
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return ok ? 0 : -1;
|
||||
|
||||
send_fail:
|
||||
directory_scanner_destroy(scanner);
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int send_files_multithreaded(Config* config) {
|
||||
if (config->dry_run) {
|
||||
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;
|
||||
}
|
||||
|
||||
Queue* q1 = queue_create(100, chunk_destroy);
|
||||
Queue* q2 = queue_create(100, chunk_destroy);
|
||||
if (!q1 || !q2) {
|
||||
if (q1)
|
||||
queue_destroy(q1);
|
||||
if (q2)
|
||||
queue_destroy(q2);
|
||||
return 1;
|
||||
}
|
||||
PipelineContextSender* context = pipeline_context_sender_create(config, q1, q2);
|
||||
if (!context) {
|
||||
queue_destroy(q1);
|
||||
queue_destroy(q2);
|
||||
return 1;
|
||||
}
|
||||
if (config->use_delete)
|
||||
context->manifest = array_list_create(free);
|
||||
|
||||
thrd_t scanner, loader, sender;
|
||||
if (thrd_create(&scanner, scan_directory_multithreaded, context) != thrd_success ||
|
||||
thrd_create(&loader, load_files_multithreaded, context) != thrd_success ||
|
||||
thrd_create(&sender, send_chunks_multithreaded, context) != thrd_success) {
|
||||
perror("Error creating threads.\n");
|
||||
pipeline_context_sender_destroy(context);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int sender_result;
|
||||
thrd_join(scanner, NULL);
|
||||
thrd_join(loader, NULL);
|
||||
thrd_join(sender, &sender_result);
|
||||
|
||||
pipeline_context_sender_destroy(context);
|
||||
return sender_result == thrd_success ? 0 : -1;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef CLIENT_SEND_H
|
||||
#define CLIENT_SEND_H
|
||||
|
||||
#include "chunk.h"
|
||||
#include "config.h"
|
||||
#include "transport_tcp.h"
|
||||
|
||||
int send_chunk(Client* client, Chunk* chunk, Config* config);
|
||||
int send_files(Config* config);
|
||||
int send_files_multithreaded(Config* config);
|
||||
|
||||
#endif
|
||||
+251
-23
@@ -1,16 +1,96 @@
|
||||
#include "scanner.h"
|
||||
#include "array_list.h"
|
||||
#include "chunk.h"
|
||||
#include "file.h"
|
||||
#include "queue.h"
|
||||
#include "utils.h"
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
DirectoryScanner *directory_scanner_create(char *root_directory) {
|
||||
DirectoryScanner* directory_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) {
|
||||
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));
|
||||
if (scanner == NULL)
|
||||
return NULL;
|
||||
scanner->directories = queue_create(100, free);
|
||||
scanner->current_dir = NULL;
|
||||
scanner->current_path = NULL;
|
||||
scanner->use_metadata = use_metadata;
|
||||
scanner->chunk_size = chunk_size > 0 ? chunk_size : DESIRED_CHUNK_SIZE;
|
||||
/* 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;
|
||||
|
||||
/* 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->max_size = max_size;
|
||||
scanner->min_size = min_size;
|
||||
scanner->follow_symlinks = follow_symlinks;
|
||||
queue_enqueue(scanner->directories, str_dup(root_directory));
|
||||
return scanner;
|
||||
}
|
||||
@@ -18,11 +98,22 @@ DirectoryScanner *directory_scanner_create(char *root_directory) {
|
||||
void directory_scanner_destroy(DirectoryScanner* scanner) {
|
||||
if (scanner == NULL)
|
||||
return;
|
||||
if (scanner->current_dir) {
|
||||
closedir(scanner->current_dir);
|
||||
scanner->current_dir = NULL;
|
||||
}
|
||||
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);
|
||||
free(scanner);
|
||||
}
|
||||
|
||||
Chunk *chunk_data_to_chunk(ArrayList *chunk_data) {
|
||||
static Chunk* chunk_data_to_chunk(ArrayList* chunk_data) {
|
||||
void** chunk_items = array_list_to_array(chunk_data);
|
||||
Chunk* chunk = chunk_create((File**)chunk_items, chunk_data->size);
|
||||
free(chunk_items);
|
||||
@@ -31,41 +122,178 @@ Chunk *chunk_data_to_chunk(ArrayList *chunk_data) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
// Returns: 1 on success, 0 if no more directories in queue, -1 on opendir failure
|
||||
static int open_next_directory(DirectoryScanner* scanner) {
|
||||
if (scanner->current_dir) {
|
||||
closedir(scanner->current_dir);
|
||||
scanner->current_dir = NULL;
|
||||
}
|
||||
free(scanner->current_path);
|
||||
|
||||
if (queue_is_empty(scanner->directories))
|
||||
return 0;
|
||||
|
||||
scanner->current_path = (char*)queue_dequeue(scanner->directories);
|
||||
scanner->current_dir = opendir(scanner->current_path);
|
||||
if (scanner->current_dir == NULL) {
|
||||
perror("Could not open directory");
|
||||
free(scanner->current_path);
|
||||
scanner->current_path = NULL;
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
ArrayList* chunk_data = array_list_create(file_destroy);
|
||||
unsigned long long chunk_data_size = 0;
|
||||
|
||||
while (!queue_is_empty(scanner->directories)) {
|
||||
char *path = (char *)queue_dequeue(scanner->directories);
|
||||
DIR *dir;
|
||||
struct dirent *entry;
|
||||
dir = opendir(path);
|
||||
if (dir == NULL) {
|
||||
perror("Could not open directory!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
|
||||
while (1) {
|
||||
if (scanner->current_dir == NULL) {
|
||||
int ret = open_next_directory(scanner);
|
||||
if (ret == 0)
|
||||
break;
|
||||
if (ret < 0)
|
||||
continue;
|
||||
}
|
||||
char *cur_path = path_cat(path, entry->d_name);
|
||||
|
||||
struct dirent* entry = readdir(scanner->current_dir);
|
||||
if (entry == NULL) {
|
||||
closedir(scanner->current_dir);
|
||||
scanner->current_dir = NULL;
|
||||
free(scanner->current_path);
|
||||
scanner->current_path = NULL;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
char* cur_path = path_cat(scanner->current_path, entry->d_name);
|
||||
struct stat stats;
|
||||
stat(cur_path, &stats);
|
||||
if (!S_ISREG(stats.st_mode))
|
||||
// Use lstat to detect symlinks
|
||||
if (lstat(cur_path, &stats) != 0) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If follow_symlinks is enabled and this is a symlink, resolve it
|
||||
if (scanner->follow_symlinks && S_ISLNK(stats.st_mode)) {
|
||||
struct stat target_stats;
|
||||
if (stat(cur_path, &target_stats) != 0) {
|
||||
// Broken symlink, skip
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
stats = target_stats;
|
||||
}
|
||||
|
||||
if (S_ISDIR(stats.st_mode)) {
|
||||
queue_enqueue(scanner->directories, (void*)cur_path);
|
||||
else {
|
||||
File *file = file_create(cur_path, &stats);
|
||||
} 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 += file->stats.st_size;
|
||||
if (chunk_data_size > DESIRED_CHUNK_SIZE)
|
||||
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;
|
||||
for (int i = 0; i < scanner->exclude_count; i++) {
|
||||
if (glob_match(scanner->exclude_patterns[i], entry->d_name)) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (excluded) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scanner->include_count > 0) {
|
||||
bool included = false;
|
||||
for (int i = 0; i < scanner->include_count; i++) {
|
||||
if (glob_match(scanner->include_patterns[i], entry->d_name)) {
|
||||
included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!included) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ((scanner->max_size > 0 && (unsigned long long)stats.st_size > scanner->max_size) ||
|
||||
(scanner->min_size > 0 && (unsigned long long)stats.st_size < scanner->min_size)) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
File* file = file_create(cur_path);
|
||||
if (file == NULL) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
file->data->size = stats.st_size;
|
||||
if (scanner->use_metadata)
|
||||
file->metadata = file_metadata_create(&stats);
|
||||
array_list_add(chunk_data, file);
|
||||
chunk_data_size += file->data->size;
|
||||
if (chunk_data_size > scanner->chunk_size) {
|
||||
free(cur_path);
|
||||
return chunk_data_to_chunk(chunk_data);
|
||||
}
|
||||
free(cur_path);
|
||||
}
|
||||
}
|
||||
closedir(dir);
|
||||
free(path);
|
||||
}
|
||||
|
||||
if (chunk_data->size > 0)
|
||||
return chunk_data_to_chunk(chunk_data);
|
||||
array_list_delete(chunk_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+24
-1
@@ -3,11 +3,34 @@
|
||||
|
||||
#include "chunk.h"
|
||||
#include "queue.h"
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
Queue* directories;
|
||||
DIR* current_dir;
|
||||
char* current_path;
|
||||
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;
|
||||
|
||||
DirectoryScanner *directory_scanner_create(char *root_directory);
|
||||
DirectoryScanner* directory_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);
|
||||
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);
|
||||
void directory_scanner_destroy(DirectoryScanner* scanner);
|
||||
|
||||
|
||||
+215
-75
@@ -1,113 +1,253 @@
|
||||
#include "array_list.h"
|
||||
#include "chunk.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "multiprocessing.h"
|
||||
#include "protocol.h"
|
||||
#include "queue.h"
|
||||
#include "socket.h"
|
||||
#include "transport_tcp.h"
|
||||
#include "transport_tls.h"
|
||||
#include "unistd.h"
|
||||
#include "utils.h"
|
||||
#include <libgen.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <threads.h>
|
||||
#include <string.h>
|
||||
|
||||
FileReceive *receive_file_receive(Config *config, int file_descriptor) {
|
||||
char *path = (char *)receive_str(file_descriptor);
|
||||
Data *file_data = receive_data(file_descriptor);
|
||||
if (config->use_compression) {
|
||||
Data *file_data_uncompressed = data_decompress(file_data);
|
||||
free(file_data);
|
||||
file_data = file_data_uncompressed;
|
||||
}
|
||||
FileReceive *file = file_receive_create(path, file_data);
|
||||
return file;
|
||||
}
|
||||
// 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);
|
||||
|
||||
int receive_thread(void *pipeline_context) {
|
||||
PipelineContextReceiver *context =
|
||||
(PipelineContextReceiver *)pipeline_context;
|
||||
mtx_lock(&context->mutex);
|
||||
int file_descriptor = context->file_descriptor;
|
||||
Config *config = context->config;
|
||||
mtx_unlock(&context->mutex);
|
||||
|
||||
while (receive_status(file_descriptor) == STATUS_NEXT) {
|
||||
FileReceive *file = receive_file_receive(config, file_descriptor);
|
||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty,
|
||||
&context->condition_not_full);
|
||||
}
|
||||
mtx_lock(&context->mutex);
|
||||
context->receiver_done = true;
|
||||
cnd_signal(&context->condition_not_empty);
|
||||
mtx_unlock(&context->mutex);
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
int write_thread(void *pipeline_context) {
|
||||
PipelineContextReceiver *context =
|
||||
(PipelineContextReceiver *)pipeline_context;
|
||||
mtx_lock(&context->mutex);
|
||||
bool save_to_disk = context->config->save_to_disk;
|
||||
char *root_directory = str_dup(context->config->receive_root_directory);
|
||||
mtx_unlock(&context->mutex);
|
||||
|
||||
while (true) {
|
||||
FileReceive *file = queue_dequeue_multithreaded(
|
||||
context->queue, &context->mutex, &context->condition_not_empty,
|
||||
&context->condition_not_full, &context->receiver_done);
|
||||
if (file == NULL) {
|
||||
free(root_directory);
|
||||
return thrd_success;
|
||||
}
|
||||
if (save_to_disk)
|
||||
to_disk(path_cat(root_directory, file->path), file->data->data,
|
||||
file->data->size);
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
int receive_files(Config *config, int file_descriptor) {
|
||||
Status status = receive_status(file_descriptor);
|
||||
while (status == STATUS_NEXT) {
|
||||
FileReceive *file = receive_file_receive(config, file_descriptor);
|
||||
if (config->save_to_disk)
|
||||
to_disk(path_cat(config->receive_root_directory, file->path),
|
||||
file->data->data, file->data->size);
|
||||
file_receive_destroy(file);
|
||||
// send_status(file_descriptor, STATUS_OK);
|
||||
status = receive_status(file_descriptor);
|
||||
// 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 (status != STATUS_FINISHED) {
|
||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED or NEXT Status");
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
}
|
||||
if (!included) {
|
||||
free(path_dup);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
free(path_dup);
|
||||
return false;
|
||||
}
|
||||
|
||||
int receive_files(Config* config, int fd) {
|
||||
Status status;
|
||||
if (!receive_status(fd, &status))
|
||||
return -1;
|
||||
|
||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) {
|
||||
if (status == STATUS_CHECK) {
|
||||
bool skipped;
|
||||
File* file = receive_incremental_check(fd, config, &skipped);
|
||||
if (skipped)
|
||||
goto next;
|
||||
if (file == NULL && !skipped)
|
||||
return -1;
|
||||
if (config->save_to_disk && !is_excluded(file->path, config))
|
||||
file_save_to_disk(config->receive_root_directory, file);
|
||||
file_destroy(file);
|
||||
} else if (status == STATUS_CHUNK) {
|
||||
Chunk* chunk = receive_chunk_data(fd, config);
|
||||
if (chunk == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
send_status(file_descriptor, STATUS_OK);
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (config->save_to_disk && !is_excluded(chunk->items[i]->path, config))
|
||||
file_save_to_disk(config->receive_root_directory, chunk->items[i]);
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
} else {
|
||||
File* file = file_receive(config, fd);
|
||||
if (file == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
if (config->save_to_disk && !is_excluded(file->path, config))
|
||||
file_save_to_disk(config->receive_root_directory, file);
|
||||
file_destroy(file);
|
||||
}
|
||||
next:
|
||||
if (!receive_status(fd, &status)) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (status == STATUS_MANIFEST) {
|
||||
if (receive_manifest(fd, config, &status) != 0)
|
||||
return -1;
|
||||
}
|
||||
if (status != STATUS_FINISHED) {
|
||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
send_status(fd, STATUS_OK);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void handler(int file_descriptor) {
|
||||
Config* config = config_receive(file_descriptor);
|
||||
if (config == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
||||
close(file_descriptor);
|
||||
return;
|
||||
}
|
||||
if (config->use_multithreading) {
|
||||
PipelineContextReceiver *context = pipeline_context_receiver_create(
|
||||
config, queue_create(100, file_receive_destroy), file_descriptor);
|
||||
Queue* q = queue_create(100, file_destroy);
|
||||
if (q == NULL) {
|
||||
config_delete(config);
|
||||
close(file_descriptor);
|
||||
return;
|
||||
}
|
||||
PipelineContextReceiver* context = pipeline_context_receiver_create(config, q, file_descriptor);
|
||||
if (context == NULL) {
|
||||
queue_destroy(q);
|
||||
config_delete(config);
|
||||
close(file_descriptor);
|
||||
return;
|
||||
}
|
||||
thrd_t receiver, writer;
|
||||
if (thrd_create(&receiver, receive_thread, context) != thrd_success ||
|
||||
thrd_create(&writer, write_thread, context) != thrd_success) {
|
||||
perror("Error creating Threads!");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("Error creating Threads");
|
||||
pipeline_context_receiver_destroy(context);
|
||||
close(file_descriptor);
|
||||
return;
|
||||
}
|
||||
thrd_join(receiver, NULL);
|
||||
thrd_join(writer, NULL);
|
||||
send_status(file_descriptor, STATUS_OK);
|
||||
pipeline_context_receiver_destroy(context);
|
||||
} else
|
||||
receive_files(config, file_descriptor);
|
||||
close(file_descriptor);
|
||||
}
|
||||
|
||||
int main() {
|
||||
Server *server = server_create(8080);
|
||||
server_listen(server, handler);
|
||||
server_delete(server);
|
||||
static Server* g_server = NULL;
|
||||
static volatile sig_atomic_t g_server_cleanup_requested = 0;
|
||||
|
||||
static void cleanup(int sig) {
|
||||
(void)sig;
|
||||
g_server_cleanup_requested = 1;
|
||||
}
|
||||
|
||||
static void print_server_usage(void) {
|
||||
printf("FastSync Server\n");
|
||||
printf("Usage: fastsync-server [options]\n");
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" --stdio Run in stdio mode (SSH transport)\n");
|
||||
printf(" -p <port> TCP port (default: 8080, range: 1-65535)\n");
|
||||
printf(" --tls Enable TLS encryption\n");
|
||||
printf(" --cert <path> TLS certificate file (PEM)\n");
|
||||
printf(" --key <path> TLS private key file (PEM)\n");
|
||||
printf(" --ca <path> TLS CA certificate file (PEM)\n");
|
||||
printf(" -v, --verbose Enable debug logging\n");
|
||||
printf(" --help Show this help\n");
|
||||
printf(" -V, --version Show version and exit\n");
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
bool use_tls = false;
|
||||
char* tls_cert = NULL;
|
||||
char* tls_key = NULL;
|
||||
char* tls_ca = NULL;
|
||||
int port = 8080;
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--help") == 0) {
|
||||
print_server_usage();
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
|
||||
printf("fastsync-server version %s\n", PROTOCOL_VERSION);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "--stdio") == 0) {
|
||||
io_set_fds(STDIN_FILENO, STDOUT_FILENO);
|
||||
handler(STDIN_FILENO);
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||
set_log_level(LOG_LEVEL_DEBUG);
|
||||
} else if (strcmp(argv[i], "--tls") == 0) {
|
||||
use_tls = true;
|
||||
} else if (strcmp(argv[i], "--cert") == 0 && i + 1 < argc) {
|
||||
tls_cert = argv[++i];
|
||||
} else if (strcmp(argv[i], "--key") == 0 && i + 1 < argc) {
|
||||
tls_key = argv[++i];
|
||||
} else if (strcmp(argv[i], "--ca") == 0 && i + 1 < argc) {
|
||||
tls_ca = argv[++i];
|
||||
} else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||
char* end;
|
||||
long p = strtol(argv[++i], &end, 10);
|
||||
if (*end || p <= 0 || p > 65535) {
|
||||
fprintf(stderr, "Error: invalid port '%s' (must be 1-65535)\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
port = (int)p;
|
||||
} else if (argv[i][0] == '-') {
|
||||
fprintf(stderr, "Unknown option: %s\n", argv[i]);
|
||||
print_server_usage();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (tls_ca && !use_tls) {
|
||||
log_message(LOG_LEVEL_WARNING, "--ca has no effect without --tls");
|
||||
}
|
||||
|
||||
signal(SIGINT, cleanup);
|
||||
signal(SIGTERM, cleanup);
|
||||
g_server = server_create(port);
|
||||
if (g_server == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to create server");
|
||||
return 1;
|
||||
}
|
||||
if (use_tls) {
|
||||
if (!tls_cert || !tls_key) {
|
||||
fprintf(stderr, "Error: --tls requires --cert and --key\n");
|
||||
server_delete(&g_server);
|
||||
return 1;
|
||||
}
|
||||
tls_global_init();
|
||||
if (!server_create_tls(g_server, tls_cert, tls_key, tls_ca)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to set up TLS");
|
||||
server_delete(&g_server);
|
||||
return 1;
|
||||
}
|
||||
server_listen_tls(g_server, handler);
|
||||
} else {
|
||||
server_listen(g_server, handler);
|
||||
}
|
||||
|
||||
/* Graceful shutdown: if a signal requested cleanup, delete the server */
|
||||
if (g_server_cleanup_requested) {
|
||||
log_message(LOG_LEVEL_INFO, "Shutdown requested, cleaning up");
|
||||
server_delete(&g_server);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
+18
-24
@@ -6,15 +6,14 @@
|
||||
ArrayList* array_list_create(void (*item_destroyer)(void* item)) {
|
||||
ArrayList* list = (ArrayList*)malloc(sizeof(ArrayList));
|
||||
if (list == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for array list struct");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("ERROR: Could not allocate memory for array list struct");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
list->items = malloc(INITIAL_ARRAY_SIZE * sizeof(void*));
|
||||
if (list->items == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for list items");
|
||||
free(list);
|
||||
exit(EXIT_FAILURE);
|
||||
return NULL;
|
||||
}
|
||||
list->size = 0;
|
||||
list->capacity = INITIAL_ARRAY_SIZE;
|
||||
@@ -35,40 +34,35 @@ void array_list_delete(ArrayList *array_list) {
|
||||
free(array_list);
|
||||
}
|
||||
|
||||
void array_list_clear(ArrayList *array_list) {
|
||||
bool array_list_extend(ArrayList* array_list) {
|
||||
if (array_list == NULL)
|
||||
return;
|
||||
for (int i = 0; i < array_list->size; i++)
|
||||
array_list->items[i] = NULL;
|
||||
array_list->size = 0;
|
||||
}
|
||||
|
||||
void array_list_extend(ArrayList *array_list) {
|
||||
if (array_list == NULL)
|
||||
return;
|
||||
return false;
|
||||
int new_capacity = array_list->capacity * 2;
|
||||
if (new_capacity == 0)
|
||||
new_capacity = INITIAL_ARRAY_SIZE;
|
||||
array_list->items = realloc(array_list->items, new_capacity * sizeof(void *));
|
||||
if (array_list->items == NULL) {
|
||||
perror("FATAL ERROR: Could not reallocate memory for array list struct");
|
||||
exit(EXIT_FAILURE);
|
||||
void* new_items = realloc(array_list->items, new_capacity * sizeof(void*));
|
||||
if (new_items == NULL) {
|
||||
perror("ERROR: Could not reallocate memory for array list items");
|
||||
return false;
|
||||
}
|
||||
array_list->items = new_items;
|
||||
array_list->capacity = new_capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
void array_list_add(ArrayList *array_list, void *item) {
|
||||
if (array_list == NULL) {
|
||||
return;
|
||||
}
|
||||
bool array_list_add(ArrayList* array_list, void* item) {
|
||||
if (array_list == NULL)
|
||||
return false;
|
||||
if (array_list->capacity == array_list->size) {
|
||||
array_list_extend(array_list);
|
||||
if (!array_list_extend(array_list))
|
||||
return false;
|
||||
}
|
||||
array_list->items[array_list->size] = item;
|
||||
array_list->size += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void **array_list_to_array(ArrayList *array_list) {
|
||||
void** array_list_to_array(const ArrayList* array_list) {
|
||||
if (array_list == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef ARRAY_LIST_H
|
||||
#define ARRAY_LIST_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define INITIAL_ARRAY_SIZE 100
|
||||
|
||||
typedef struct ArrayList {
|
||||
@@ -12,9 +14,8 @@ typedef struct ArrayList {
|
||||
|
||||
ArrayList* array_list_create(void (*item_destroyer)(void* item));
|
||||
void array_list_delete(ArrayList* array_list);
|
||||
void array_list_clear(ArrayList *array_list);
|
||||
void array_list_extend(ArrayList *array_list);
|
||||
void array_list_add(ArrayList *array_list, void *item);
|
||||
void **array_list_to_array(ArrayList *array_list);
|
||||
bool array_list_extend(ArrayList* array_list);
|
||||
bool array_list_add(ArrayList* array_list, void* item);
|
||||
void** array_list_to_array(const ArrayList* array_list);
|
||||
|
||||
#endif
|
||||
|
||||
+151
-132
@@ -1,30 +1,28 @@
|
||||
#include <dirent.h>
|
||||
#include <libgen.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
|
||||
#include "array_list.h"
|
||||
#include "chunk.h"
|
||||
#include "compression.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
|
||||
Chunk* chunk_create(File** items, int element_count) {
|
||||
Chunk* chunk = (Chunk*)malloc(sizeof(Chunk));
|
||||
if (chunk == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for chunk structure");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("ERROR: Could not allocate memory for chunk structure");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
chunk->items = (File**)malloc(element_count * sizeof(File*));
|
||||
if (chunk->items == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for items of chunk "
|
||||
"structure");
|
||||
free(chunk);
|
||||
exit(EXIT_FAILURE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < element_count; i++) {
|
||||
@@ -48,152 +46,173 @@ void chunk_destroy(void *item) {
|
||||
free(chunk);
|
||||
}
|
||||
|
||||
void chunk_print(void *item) {
|
||||
if (item == NULL)
|
||||
return;
|
||||
Chunk *chunk = (Chunk *)item;
|
||||
for (int i = 0; i < chunk->element_count; ++i)
|
||||
if (chunk->items[i] != NULL)
|
||||
file_print(chunk->items[i]);
|
||||
static unsigned long long per_file_serialize_size(File* file, bool use_metadata) {
|
||||
return sizeof(size_t) + strlen(file->path) +
|
||||
(use_metadata ? sizeof(int) + (file->metadata ? FILE_METADATA_WIRE_SIZE : 0) : 0) +
|
||||
sizeof(size_t) + file->data->size;
|
||||
}
|
||||
|
||||
Data *chunk_format(Chunk *chunk) {
|
||||
unsigned long long buffer_size = 0;
|
||||
for (int i = 0; i < chunk->element_count; ++i) {
|
||||
buffer_size += sizeof(int);
|
||||
buffer_size += strlen(chunk->items[i]->path);
|
||||
buffer_size += sizeof(unsigned long long);
|
||||
buffer_size += chunk->items[i]->stats.st_size;
|
||||
}
|
||||
|
||||
char *data = malloc(buffer_size);
|
||||
if (data == NULL) {
|
||||
perror("Could not allocate data for ChunkFormated!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
char *current_data_pointer = data;
|
||||
// for (int i = 0; i < chunk->element_count; ++i) {
|
||||
// File *file = chunk->items[i];
|
||||
// // add path len
|
||||
// int path_length = (int)strlen(file->path);
|
||||
// memcpy(current_data_pointer, &path_length, sizeof(int));
|
||||
// current_data_pointer += sizeof(int);
|
||||
// // add path
|
||||
// memcpy(current_data_pointer, file->path, path_length);
|
||||
// current_data_pointer += path_length;
|
||||
// // add file data len
|
||||
// unsigned long long file_length = file->stats.st_size;
|
||||
// memcpy(current_data_pointer, &file_length, sizeof(unsigned long long));
|
||||
// current_data_pointer += sizeof(unsigned long long);
|
||||
// // add file data
|
||||
// file_content_to_buffer(file, current_data_pointer);
|
||||
// current_data_pointer += file_length;
|
||||
// }
|
||||
if (current_data_pointer - data != (long)(long)buffer_size) {
|
||||
perror("Buffer of Chunk wasn't filled enough!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
return chunk_data_create(data, buffer_size);
|
||||
}
|
||||
|
||||
Data *chunk_compress(Chunk *chunk, int compression_level) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to gather data for chunk compression");
|
||||
Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
||||
unsigned long long data_size = 0;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
data_size += sizeof(unsigned long long);
|
||||
data_size += strlen(chunk->items[i]->path);
|
||||
data_size += sizeof(unsigned long long);
|
||||
data_size += chunk->items[i]->stats.st_size;
|
||||
data_size += per_file_serialize_size(chunk->items[i], use_metadata);
|
||||
}
|
||||
Data* data = data_create_empty(data_size);
|
||||
if (data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"Could not allocate memory for chunk compression");
|
||||
exit(EXIT_FAILURE);
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for chunk serialization");
|
||||
return NULL;
|
||||
}
|
||||
char* data_pointer = data->data;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
// path length
|
||||
size_t path_len = strlen(chunk->items[i]->path);
|
||||
File* file = chunk->items[i];
|
||||
size_t path_len = strlen(file->path);
|
||||
memcpy(data_pointer, &path_len, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
memcpy(data_pointer, chunk->items[i]->path, path_len);
|
||||
memcpy(data_pointer, file->path, path_len);
|
||||
data_pointer += path_len;
|
||||
// file data
|
||||
unsigned long long data_size = chunk->items[i]->stats.st_size;
|
||||
memcpy(data_pointer, &data_size, sizeof(size_t));
|
||||
|
||||
if (use_metadata)
|
||||
metadata_to_buf(&data_pointer, file->metadata);
|
||||
|
||||
size_t file_data_size = file->data->size;
|
||||
memcpy(data_pointer, &file_data_size, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
memcpy(data_pointer, chunk->items[i]->data, data_size);
|
||||
data_pointer += data_size;
|
||||
memcpy(data_pointer, file->data->data, file_data_size);
|
||||
data_pointer += file_data_size;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "Chunk succesfully compressed");
|
||||
return data_compress(data, compression_level);
|
||||
}
|
||||
|
||||
Chunk *chunk_decompress(Data *compressed_data) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to decompress chunk");
|
||||
Data *uncompressed_data = data_decompress(compressed_data);
|
||||
Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
ArrayList* files = array_list_create(file_destroy);
|
||||
size_t *data_pointer = uncompressed_data->data;
|
||||
while (data_pointer <
|
||||
(size_t *)uncompressed_data->data + uncompressed_data->size) {
|
||||
size_t path_len = data_pointer[0];
|
||||
char* data_pointer = data->data;
|
||||
size_t remaining_size = data->size;
|
||||
|
||||
printf("%zu, testing", path_len);
|
||||
break;
|
||||
}
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "Chunk succesfully decompressed");
|
||||
while (remaining_size > 0) {
|
||||
if (remaining_size < sizeof(size_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for path length");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data *chunk_data_create(void *data, unsigned long long data_size) {
|
||||
Data *chunk_formated = malloc(sizeof(Data));
|
||||
if (chunk_formated == NULL) {
|
||||
perror("Could not allocate memory for ChunkFormated");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
chunk_formated->data = data;
|
||||
chunk_formated->size = data_size;
|
||||
return chunk_formated;
|
||||
size_t path_len;
|
||||
memcpy(&path_len, data_pointer, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
remaining_size -= sizeof(size_t);
|
||||
|
||||
if (remaining_size < path_len) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for path");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void chunk_data_delete(void *chunk) {
|
||||
Data *chunk_data = (Data *)chunk;
|
||||
free(chunk_data->data);
|
||||
free(chunk_data);
|
||||
char* path = malloc(path_len + 1);
|
||||
if (path == NULL) {
|
||||
perror("Could not allocate memory for file path");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(path, data_pointer, path_len);
|
||||
path[path_len] = '\0';
|
||||
data_pointer += path_len;
|
||||
remaining_size -= path_len;
|
||||
|
||||
File* file = file_create(path);
|
||||
free(path);
|
||||
|
||||
if (use_metadata) {
|
||||
if (remaining_size < sizeof(int)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
// Peek at present flag to determine total size needed before reading
|
||||
int present_flag;
|
||||
memcpy(&present_flag, data_pointer, sizeof(int));
|
||||
if (present_flag && remaining_size < sizeof(int) + FILE_METADATA_WIRE_SIZE) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for metadata body");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
file->metadata = metadata_from_buf(&data_pointer);
|
||||
remaining_size -= sizeof(int);
|
||||
if (file->metadata)
|
||||
remaining_size -= FILE_METADATA_WIRE_SIZE;
|
||||
}
|
||||
|
||||
// void chunk_data_to_disk(ChunkData *chunk_formated, char *root_directory) {
|
||||
// char *current_data_pointer = chunk_formated->data;
|
||||
// while (current_data_pointer - (char *)chunk_formated->data <
|
||||
// chunk_formated->data_size) {
|
||||
// // get path length
|
||||
// int path_length = 0;
|
||||
// memcpy(&path_length, (int *)current_data_pointer, sizeof(int));
|
||||
// current_data_pointer += sizeof(int);
|
||||
// // get path
|
||||
// int path_dir_size =
|
||||
// (strlen(root_directory) + path_length + 1) * sizeof(char);
|
||||
// char *path = (char *)malloc(path_dir_size);
|
||||
// if (path == NULL) {
|
||||
// perror("Could not allocate memory for path!");
|
||||
// exit(EXIT_FAILURE);
|
||||
// }
|
||||
// snprintf(path, path_dir_size, "%s%.*s", root_directory, path_length,
|
||||
// current_data_pointer);
|
||||
// current_data_pointer += sizeof(char) * path_length;
|
||||
// // get data length
|
||||
// unsigned long long data_size = 0;
|
||||
// memcpy(&data_size, (unsigned long long *)current_data_pointer,
|
||||
// sizeof(unsigned long long));
|
||||
// current_data_pointer += sizeof(unsigned long long);
|
||||
// // create File Receive
|
||||
// FileReceive *file =
|
||||
// file_receive_create(path, data_size, current_data_pointer);
|
||||
// file_receive_print(file);
|
||||
// file_receive_to_disk(file);
|
||||
// current_data_pointer += sizeof(char) * data_size;
|
||||
// }
|
||||
// }
|
||||
if (remaining_size < sizeof(size_t)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for data size");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t file_data_size;
|
||||
memcpy(&file_data_size, data_pointer, sizeof(size_t));
|
||||
data_pointer += sizeof(size_t);
|
||||
remaining_size -= sizeof(size_t);
|
||||
|
||||
if (remaining_size < file_data_size) {
|
||||
log_message(LOG_LEVEL_ERROR, "Invalid chunk format: not enough data for file content");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* file_data = malloc(file_data_size);
|
||||
if (file_data == NULL) {
|
||||
perror("Could not allocate memory for file data");
|
||||
array_list_delete(files);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(file_data, data_pointer, file_data_size);
|
||||
data_destroy(file->data);
|
||||
file->data = data_create(file_data, file_data_size);
|
||||
data_pointer += file_data_size;
|
||||
remaining_size -= file_data_size;
|
||||
|
||||
array_list_add(files, file);
|
||||
}
|
||||
|
||||
File** file_array = (File**)array_list_to_array(files);
|
||||
Chunk* chunk = chunk_create(file_array, files->size);
|
||||
|
||||
free(file_array);
|
||||
files->item_destroyer = NULL;
|
||||
array_list_delete(files);
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
Data* chunk_compress(Chunk* chunk, int compression_level, bool use_metadata) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress chunk");
|
||||
Data* serialized = chunk_serialize(chunk, use_metadata);
|
||||
if (serialized == NULL)
|
||||
return NULL;
|
||||
Data* compressed = data_compress(serialized, compression_level);
|
||||
data_destroy(serialized);
|
||||
if (compressed == NULL)
|
||||
return NULL;
|
||||
log_message(LOG_LEVEL_DEBUG, "Chunk successfully compressed");
|
||||
return compressed;
|
||||
}
|
||||
|
||||
Chunk* receive_chunk_data(int fd, const Config* config) {
|
||||
Data* chunk_data = receive_data(fd);
|
||||
if (chunk_data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to receive chunk data");
|
||||
return NULL;
|
||||
}
|
||||
Data* data_to_process = chunk_data;
|
||||
if (config->use_compression) {
|
||||
data_to_process = data_decompress(chunk_data);
|
||||
data_destroy(chunk_data);
|
||||
if (data_to_process == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to decompress chunk");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
Chunk* chunk = chunk_deserialize(data_to_process, config->use_metadata);
|
||||
data_destroy(data_to_process);
|
||||
if (chunk == NULL)
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to deserialize chunk, skipping");
|
||||
return chunk;
|
||||
}
|
||||
|
||||
+7
-10
@@ -1,13 +1,13 @@
|
||||
#ifndef CHUNK_H
|
||||
#define CHUNK_H
|
||||
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define DESIRED_CHUNK_SIZE 10 * 1024 * 1024
|
||||
#define FILE_PATH_SEPERATOR "#&&SEPP&&#"
|
||||
#define FILE_PATH_DATA_SEPERATOR "#&&SEPD&&#"
|
||||
#define DESIRED_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
|
||||
typedef struct {
|
||||
File** items;
|
||||
@@ -16,12 +16,9 @@ typedef struct {
|
||||
|
||||
Chunk* chunk_create(File** items, int element_count);
|
||||
void chunk_destroy(void* chunk);
|
||||
void chunk_print(void *chunk);
|
||||
Data *chunk_format(Chunk *chunk);
|
||||
Data *chunk_compress(Chunk *chunk, int compression_level);
|
||||
Chunk *chunk_decompress(Data *compressed_data);
|
||||
Data* chunk_serialize(Chunk* chunk, bool use_metadata);
|
||||
Chunk* chunk_deserialize(Data* data, bool use_metadata);
|
||||
Data* chunk_compress(Chunk* chunk, int compression_level, bool use_metadata);
|
||||
Chunk* receive_chunk_data(int fd, const Config* config);
|
||||
|
||||
Data *chunk_data_create(void *data, unsigned long long data_size);
|
||||
void chunk_data_delete(void *chunk);
|
||||
void chunk_data_to_disk(Data *chunk, char *root_directory);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "compression.h"
|
||||
#include "data.h"
|
||||
#include "log.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include "zstd.h"
|
||||
|
||||
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
|
||||
|
||||
/* Clamp compression level to valid zstd range [1, 22] */
|
||||
if (compression_level < 1) {
|
||||
log_message(LOG_LEVEL_WARNING, "compression_level %d out of range [1,22], using 1",
|
||||
compression_level);
|
||||
compression_level = 1;
|
||||
} else if (compression_level > 22) {
|
||||
log_message(LOG_LEVEL_WARNING, "compression_level %d out of range [1,22], using 22",
|
||||
compression_level);
|
||||
compression_level = 22;
|
||||
}
|
||||
|
||||
size_t dst_size = ZSTD_compressBound(data_to_compress->size);
|
||||
Data* compressed_data = data_create_empty(dst_size);
|
||||
if (compressed_data == NULL)
|
||||
return NULL;
|
||||
|
||||
ZSTD_CCtx* cctx = ZSTD_createCCtx();
|
||||
if (!cctx) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to create ZSTD compression context");
|
||||
data_destroy(compressed_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t zret = ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, compression_level);
|
||||
if (ZSTD_isError(zret)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to set compression level: %s", ZSTD_getErrorName(zret));
|
||||
ZSTD_freeCCtx(cctx);
|
||||
data_destroy(compressed_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ZSTD_inBuffer input = {data_to_compress->data, data_to_compress->size, 0};
|
||||
ZSTD_outBuffer output = {compressed_data->data, dst_size, 0};
|
||||
|
||||
size_t ret;
|
||||
do {
|
||||
ret = ZSTD_compressStream2(cctx, &output, &input, ZSTD_e_end);
|
||||
if (ZSTD_isError(ret)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Compression failed: %s", ZSTD_getErrorName(ret));
|
||||
ZSTD_freeCCtx(cctx);
|
||||
data_destroy(compressed_data);
|
||||
return NULL;
|
||||
}
|
||||
} while (ret > 0);
|
||||
|
||||
compressed_data->size = output.pos;
|
||||
ZSTD_freeCCtx(cctx);
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "Data succesfully compressed from %zu to %zu",
|
||||
data_to_compress->size, compressed_data->size);
|
||||
return compressed_data;
|
||||
}
|
||||
|
||||
Data* data_decompress(Data* compressed_data) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Start to decompress data");
|
||||
unsigned long long dst_size =
|
||||
ZSTD_getFrameContentSize(compressed_data->data, compressed_data->size);
|
||||
if (ZSTD_isError(dst_size)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to get decompressed size: %s",
|
||||
ZSTD_getErrorName(dst_size));
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ZSTD_DCtx* dctx = ZSTD_createDCtx();
|
||||
if (!dctx) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to create ZSTD decompression context");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t buf_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);
|
||||
if (!uncompressed_data) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to allocate decompression buffer");
|
||||
ZSTD_freeDCtx(dctx);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ZSTD_inBuffer input = {compressed_data->data, compressed_data->size, 0};
|
||||
ZSTD_outBuffer output = {uncompressed_data->data, buf_size, 0};
|
||||
|
||||
size_t ret;
|
||||
do {
|
||||
ret = ZSTD_decompressStream(dctx, &output, &input);
|
||||
if (ZSTD_isError(ret)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Decompression failed: %s", ZSTD_getErrorName(ret));
|
||||
ZSTD_freeDCtx(dctx);
|
||||
data_destroy(uncompressed_data);
|
||||
return NULL;
|
||||
}
|
||||
if (ret > 0 && output.pos == output.size) {
|
||||
buf_size *= 2;
|
||||
void* new_data = realloc(uncompressed_data->data, buf_size);
|
||||
if (!new_data) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to grow decompression buffer");
|
||||
ZSTD_freeDCtx(dctx);
|
||||
data_destroy(uncompressed_data);
|
||||
return NULL;
|
||||
}
|
||||
uncompressed_data->data = new_data;
|
||||
output.dst = new_data;
|
||||
output.size = buf_size;
|
||||
}
|
||||
} while (ret > 0);
|
||||
|
||||
uncompressed_data->size = output.pos;
|
||||
ZSTD_freeDCtx(dctx);
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "Decompressed data successfully");
|
||||
return uncompressed_data;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#ifndef COMPRESSION_H
|
||||
#define COMPRESSION_H
|
||||
|
||||
#include "data.h"
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level);
|
||||
Data* data_decompress(Data* compressed_data);
|
||||
|
||||
#endif
|
||||
+291
-30
@@ -1,14 +1,17 @@
|
||||
#include "config.h"
|
||||
#include "socket.h"
|
||||
#include "delta.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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, int compression_level,
|
||||
int num_connections) {
|
||||
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) {
|
||||
|
||||
Config* config = malloc(sizeof(Config));
|
||||
config->version = version;
|
||||
@@ -18,45 +21,303 @@ Config *config_create(char *version, char *send_directory,
|
||||
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->num_connections = num_connections;
|
||||
config->use_sendfile = use_sendfile;
|
||||
config->chunk_size = chunk_size > 0 ? chunk_size : DEFAULT_CHUNK_SIZE;
|
||||
config->ssh_port = 22;
|
||||
config->transport = TRANSPORT_TCP;
|
||||
config->ssh_destination = NULL;
|
||||
config->exclude_patterns = NULL;
|
||||
config->exclude_count = 0;
|
||||
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_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;
|
||||
}
|
||||
|
||||
bool is_remote_dest(const char* s) {
|
||||
if (s == NULL)
|
||||
return false;
|
||||
const char* colon = strchr(s, ':');
|
||||
if (colon == NULL)
|
||||
return false;
|
||||
if (colon == s)
|
||||
return false;
|
||||
for (const char* p = s; p < colon; p++) {
|
||||
if (*p == '/')
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void config_parse_ssh_dest(Config* config) {
|
||||
if (!is_remote_dest(config->receive_root_directory))
|
||||
return;
|
||||
config->transport = TRANSPORT_SSH;
|
||||
config->ssh_destination = str_dup(config->receive_root_directory);
|
||||
const char* colon = strchr(config->receive_root_directory, ':');
|
||||
char* path = str_dup(colon + 1);
|
||||
free(config->receive_root_directory);
|
||||
config->receive_root_directory = path;
|
||||
}
|
||||
|
||||
void config_delete(Config* config) {
|
||||
free(config->version);
|
||||
free(config->send_directory);
|
||||
free(config->receive_root_directory);
|
||||
free(config->ssh_destination);
|
||||
for (int i = 0; i < config->exclude_count; i++)
|
||||
free(config->exclude_patterns[i]);
|
||||
free(config->exclude_patterns);
|
||||
for (int i = 0; i < config->include_count; i++)
|
||||
free(config->include_patterns[i]);
|
||||
free(config->include_patterns);
|
||||
free(config->tls_cert);
|
||||
free(config->tls_key);
|
||||
free(config->tls_ca);
|
||||
free(config->server_host);
|
||||
free(config);
|
||||
}
|
||||
|
||||
void config_send(int file_descriptor, Config *config) {
|
||||
send_str(file_descriptor, config->version);
|
||||
send_str(file_descriptor, config->send_directory);
|
||||
send_str(file_descriptor, config->receive_root_directory);
|
||||
send_int(file_descriptor, config->save_to_disk);
|
||||
send_int(file_descriptor, config->use_multithreading);
|
||||
send_int(file_descriptor, config->use_chunk_serialization);
|
||||
send_int(file_descriptor, config->use_compression);
|
||||
send_int(file_descriptor, config->use_compression);
|
||||
send_int(file_descriptor, config->num_connections);
|
||||
if (receive_status(file_descriptor) != STATUS_OK) {
|
||||
perror("Error transmitting config!");
|
||||
exit(EXIT_FAILURE);
|
||||
bool config_send(int file_descriptor, const Config* config) {
|
||||
if (!send_str(file_descriptor, config->version))
|
||||
return false;
|
||||
if (!send_str(file_descriptor, config->send_directory))
|
||||
return false;
|
||||
if (!send_str(file_descriptor, config->receive_root_directory))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->save_to_disk))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_multithreading))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_chunk_serialization))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_compression))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_metadata))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->compression_level))
|
||||
return false;
|
||||
if (!send_n_data(file_descriptor, &config->chunk_size, sizeof(config->chunk_size)))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_sendfile))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_delete))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_incremental))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->use_delta))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, (int)config->delta_block_size))
|
||||
return false;
|
||||
if (!send_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long)))
|
||||
return false;
|
||||
if (!send_int(file_descriptor, config->exclude_count))
|
||||
return false;
|
||||
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;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return false;
|
||||
if (status != STATUS_OK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Config* config_receive(int file_descriptor) {
|
||||
Config* config = (Config*)malloc(sizeof(Config));
|
||||
if (config == NULL)
|
||||
return NULL;
|
||||
memset(config, 0, sizeof(*config));
|
||||
config->version = receive_str(file_descriptor);
|
||||
config->send_directory = receive_str(file_descriptor);
|
||||
config->receive_root_directory = receive_str(file_descriptor);
|
||||
config->save_to_disk = receive_int(file_descriptor);
|
||||
config->use_multithreading = receive_int(file_descriptor);
|
||||
config->use_chunk_serialization = receive_int(file_descriptor);
|
||||
config->use_compression = receive_int(file_descriptor);
|
||||
config->compression_level = receive_int(file_descriptor);
|
||||
config->num_connections = receive_int(file_descriptor);
|
||||
send_status(file_descriptor, STATUS_OK);
|
||||
return config;
|
||||
if (!config->version) {
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
if (strcmp(config->version, PROTOCOL_VERSION) != 0) {
|
||||
fprintf(stderr, "Protocol version mismatch: client=%s, server=%s\n", config->version,
|
||||
PROTOCOL_VERSION);
|
||||
free(config->version);
|
||||
free(config);
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
config->send_directory = receive_str(file_descriptor);
|
||||
if (!config->send_directory) {
|
||||
free(config->version);
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
config->receive_root_directory = receive_str(file_descriptor);
|
||||
if (!config->receive_root_directory) {
|
||||
free(config->version);
|
||||
free(config->send_directory);
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
int tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->save_to_disk = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_multithreading = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_chunk_serialization = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_compression = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_metadata = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->compression_level = tmp;
|
||||
if (!receive_n_data(file_descriptor, &config->chunk_size, sizeof(config->chunk_size)))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_sendfile = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_delete = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_incremental = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->use_delta = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
config->delta_block_size = (uint32_t)tmp;
|
||||
if (!receive_n_data(file_descriptor, &config->delta_max_file_size, sizeof(unsigned long long)))
|
||||
goto error;
|
||||
config->show_progress = false;
|
||||
config->dry_run = false;
|
||||
config->ssh_port = 22;
|
||||
config->transport = TRANSPORT_TCP;
|
||||
config->ssh_destination = NULL;
|
||||
config->exclude_patterns = NULL;
|
||||
config->exclude_count = 0;
|
||||
config->include_patterns = NULL;
|
||||
config->include_count = 0;
|
||||
config->max_size = 0;
|
||||
config->min_size = 0;
|
||||
config->use_tls = false;
|
||||
config->tls_cert = NULL;
|
||||
config->tls_key = NULL;
|
||||
config->tls_ca = NULL;
|
||||
config->follow_symlinks = false;
|
||||
config->partial = false;
|
||||
|
||||
// Receive exclude patterns
|
||||
int ec;
|
||||
if (!receive_int(file_descriptor, &ec))
|
||||
goto error;
|
||||
config->exclude_count = ec;
|
||||
if (ec > 0) {
|
||||
config->exclude_patterns = malloc((size_t)ec * sizeof(char*));
|
||||
if (!config->exclude_patterns) {
|
||||
config->exclude_count = 0;
|
||||
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_port = 8080;
|
||||
if (!send_status(file_descriptor, STATUS_OK))
|
||||
goto error;
|
||||
return config;
|
||||
|
||||
error:
|
||||
free(config->version);
|
||||
free(config->send_directory);
|
||||
free(config->receive_root_directory);
|
||||
free(config->server_host);
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+40
-8
@@ -2,6 +2,9 @@
|
||||
#define CONFIG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
|
||||
|
||||
typedef struct Config {
|
||||
char* version;
|
||||
@@ -11,18 +14,47 @@ typedef struct Config {
|
||||
bool use_multithreading;
|
||||
bool use_chunk_serialization;
|
||||
bool use_compression;
|
||||
bool use_single_send_per_file;
|
||||
bool use_sendfile;
|
||||
bool use_metadata;
|
||||
bool show_progress;
|
||||
bool dry_run;
|
||||
bool use_delete;
|
||||
int compression_level;
|
||||
int num_connections;
|
||||
unsigned long long chunk_size;
|
||||
int ssh_port;
|
||||
TransportType transport;
|
||||
char* ssh_destination;
|
||||
char** exclude_patterns;
|
||||
int exclude_count;
|
||||
char** include_patterns;
|
||||
int include_count;
|
||||
unsigned long long max_size;
|
||||
unsigned long long min_size;
|
||||
bool use_incremental;
|
||||
bool use_delta;
|
||||
uint32_t delta_block_size;
|
||||
unsigned long long delta_max_file_size;
|
||||
bool use_tls;
|
||||
char* server_host;
|
||||
int server_port;
|
||||
char* tls_cert;
|
||||
char* tls_key;
|
||||
char* tls_ca;
|
||||
bool follow_symlinks;
|
||||
bool partial;
|
||||
} Config;
|
||||
|
||||
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, int compression_level,
|
||||
int num_connections);
|
||||
#define PROTOCOL_VERSION "1.3.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
|
||||
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_send(int file_descriptor, Config *config);
|
||||
bool config_send(int file_descriptor, const Config* config);
|
||||
Config* config_receive(int file_descriptor);
|
||||
bool is_remote_dest(const char* s);
|
||||
void config_parse_ssh_dest(Config* config);
|
||||
|
||||
#endif
|
||||
|
||||
+19
-46
@@ -1,23 +1,35 @@
|
||||
|
||||
#include "data.h"
|
||||
#include "log.h"
|
||||
#include "stdlib.h"
|
||||
#include "zstd.h"
|
||||
|
||||
Data* data_create_empty(size_t data_size) {
|
||||
void *data = malloc(data_size);
|
||||
/* malloc(0) is UB; allocate at least 1 byte but preserve requested size */
|
||||
size_t alloc_size = data_size > 0 ? data_size : 1;
|
||||
void* data = malloc(alloc_size);
|
||||
if (data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for empty data");
|
||||
exit(EXIT_FAILURE);
|
||||
return NULL;
|
||||
}
|
||||
return data_create(data, data_size);
|
||||
}
|
||||
|
||||
Data* data_create_reserve(size_t size) {
|
||||
Data* d = malloc(sizeof(Data));
|
||||
if (d == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for data");
|
||||
return NULL;
|
||||
}
|
||||
d->data = NULL;
|
||||
d->size = size;
|
||||
return d;
|
||||
}
|
||||
|
||||
Data* data_create(void* data, size_t data_size) {
|
||||
Data* new_data = malloc(sizeof(Data));
|
||||
if (new_data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for data");
|
||||
exit(EXIT_FAILURE);
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
new_data->data = data;
|
||||
new_data->size = data_size;
|
||||
@@ -25,47 +37,8 @@ Data *data_create(void *data, size_t data_size) {
|
||||
}
|
||||
|
||||
void data_destroy(Data* data) {
|
||||
if (data == NULL)
|
||||
return;
|
||||
free(data->data);
|
||||
free(data);
|
||||
}
|
||||
|
||||
Data *data_compress(Data *data_to_compress, int compression_level) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
|
||||
Data *compressed_data =
|
||||
data_create_empty(ZSTD_compressBound(data_to_compress->size));
|
||||
|
||||
compressed_data->size = ZSTD_compress(
|
||||
compressed_data->data, compressed_data->size, data_to_compress->data,
|
||||
data_to_compress->size, compression_level);
|
||||
if (ZSTD_isError(compressed_data->size)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Compression failed: %s",
|
||||
ZSTD_getErrorName(compressed_data->size));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "Data succesfully compressed from %zu to %zu",
|
||||
data_to_compress->size, compressed_data->size);
|
||||
return compressed_data;
|
||||
}
|
||||
|
||||
Data *data_decompress(Data *compressed_data) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Start to decompress data");
|
||||
Data *uncompressed_data = data_create_empty(
|
||||
ZSTD_getFrameContentSize(compressed_data->data, compressed_data->size));
|
||||
if (ZSTD_isError(uncompressed_data->size)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Decompression failed: %s",
|
||||
ZSTD_getErrorName(uncompressed_data->size));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
uncompressed_data->size =
|
||||
ZSTD_decompress(uncompressed_data->data, uncompressed_data->size,
|
||||
compressed_data->data, compressed_data->size);
|
||||
if (ZSTD_isError(uncompressed_data->size)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Decompression failed: %s",
|
||||
ZSTD_getErrorName(uncompressed_data->size));
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, "Decompressed data successfully");
|
||||
return uncompressed_data;
|
||||
}
|
||||
|
||||
+2
-3
@@ -1,7 +1,7 @@
|
||||
#ifndef DATA_H
|
||||
#define DATA_H
|
||||
|
||||
#include "stdlib.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct {
|
||||
void* data;
|
||||
@@ -9,9 +9,8 @@ typedef struct {
|
||||
} Data;
|
||||
|
||||
Data* data_create_empty(size_t data_size);
|
||||
Data* data_create_reserve(size_t size);
|
||||
Data* data_create(void* data, size_t data_size);
|
||||
void data_destroy(Data* data);
|
||||
Data *data_compress(Data *data_to_compress, int compression_level);
|
||||
Data *data_decompress(Data *compressed_data);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
#include "delta.h"
|
||||
#include "log.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define XXH_STATIC_LINKING_ONLY
|
||||
#define XXH_IMPLEMENTATION
|
||||
#include <xxhash.h>
|
||||
|
||||
uint32_t delta_adler32(const void* data, uint32_t len) {
|
||||
const uint8_t* p = (const uint8_t*)data;
|
||||
uint32_t s1 = 1;
|
||||
uint32_t s2 = 0;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
s1 = (s1 + p[i]) % DELTA_ADLER32_MODULUS;
|
||||
s2 = (s2 + s1) % DELTA_ADLER32_MODULUS;
|
||||
}
|
||||
return (s2 << 16) | s1;
|
||||
}
|
||||
|
||||
uint32_t delta_xxhash32(const void* data, uint32_t len) {
|
||||
return XXH32(data, len, 0);
|
||||
}
|
||||
|
||||
DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_file_size,
|
||||
uint32_t block_size) {
|
||||
if (old_file_data == NULL || old_file_size == 0 || block_size == 0)
|
||||
return NULL;
|
||||
|
||||
uint32_t block_count = (uint32_t)((old_file_size + block_size - 1) / block_size);
|
||||
|
||||
DeltaSignature* sig = malloc(sizeof(DeltaSignature));
|
||||
if (!sig)
|
||||
return NULL;
|
||||
|
||||
sig->file_size = old_file_size;
|
||||
sig->block_size = block_size;
|
||||
sig->block_count = block_count;
|
||||
sig->blocks = malloc(block_count * sizeof(DeltaBlockSig));
|
||||
if (!sig->blocks) {
|
||||
free(sig);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const uint8_t* data = (const uint8_t*)old_file_data;
|
||||
for (uint32_t i = 0; i < block_count; i++) {
|
||||
uint64_t offset = (uint64_t)i * block_size;
|
||||
uint32_t len =
|
||||
(uint32_t)((old_file_size - offset < block_size) ? (old_file_size - offset) : block_size);
|
||||
sig->blocks[i].adler32 = delta_adler32(data + offset, len);
|
||||
sig->blocks[i].xxhash = delta_xxhash32(data + offset, len);
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
Data* delta_signature_serialize(const DeltaSignature* sig) {
|
||||
if (!sig)
|
||||
return NULL;
|
||||
|
||||
uint64_t total = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
(uint64_t)sig->block_count * (sizeof(uint32_t) + sizeof(uint32_t));
|
||||
|
||||
uint8_t* buf = malloc((size_t)total);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
|
||||
size_t pos = 0;
|
||||
memcpy(buf + pos, &sig->file_size, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
memcpy(buf + pos, &sig->block_size, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(buf + pos, &sig->block_count, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
|
||||
for (uint32_t i = 0; i < sig->block_count; i++) {
|
||||
memcpy(buf + pos, &sig->blocks[i].adler32, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(buf + pos, &sig->blocks[i].xxhash, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
}
|
||||
|
||||
return data_create(buf, (size_t)total);
|
||||
}
|
||||
|
||||
DeltaSignature* delta_signature_deserialize(const Data* data) {
|
||||
if (!data || data->size < sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t))
|
||||
return NULL;
|
||||
|
||||
const uint8_t* buf = (const uint8_t*)data->data;
|
||||
size_t pos = 0;
|
||||
|
||||
DeltaSignature* sig = malloc(sizeof(DeltaSignature));
|
||||
if (!sig)
|
||||
return NULL;
|
||||
|
||||
memcpy(&sig->file_size, buf + pos, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
memcpy(&sig->block_size, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(&sig->block_count, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
|
||||
uint64_t expected = sizeof(uint64_t) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
(uint64_t)sig->block_count * (sizeof(uint32_t) + sizeof(uint32_t));
|
||||
if (data->size < expected) {
|
||||
free(sig);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sig->blocks = malloc(sig->block_count * sizeof(DeltaBlockSig));
|
||||
if (!sig->blocks) {
|
||||
free(sig);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < sig->block_count; i++) {
|
||||
memcpy(&sig->blocks[i].adler32, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(&sig->blocks[i].xxhash, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
}
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
void delta_signature_destroy(DeltaSignature* sig) {
|
||||
if (!sig)
|
||||
return;
|
||||
free(sig->blocks);
|
||||
free(sig);
|
||||
}
|
||||
|
||||
static bool ensure_capacity(DeltaInstruction** instrs, uint32_t* capacity, uint32_t count) {
|
||||
if (count < *capacity)
|
||||
return true;
|
||||
uint32_t new_cap = *capacity * 2;
|
||||
DeltaInstruction* tmp = realloc(*instrs, new_cap * sizeof(DeltaInstruction));
|
||||
if (!tmp)
|
||||
return false;
|
||||
*instrs = tmp;
|
||||
*capacity = new_cap;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool flush_literal(DeltaInstruction** instrs, uint32_t* capacity, uint32_t* count,
|
||||
const uint8_t* data, uint64_t start, uint64_t end) {
|
||||
if (start >= end)
|
||||
return true;
|
||||
uint32_t lit_len = (uint32_t)(end - start);
|
||||
if (!ensure_capacity(instrs, capacity, *count))
|
||||
return false;
|
||||
uint8_t* lit_data = malloc(lit_len);
|
||||
if (!lit_data)
|
||||
return false;
|
||||
memcpy(lit_data, data + start, lit_len);
|
||||
(*instrs)[*count].type = DELTA_INSTR_LITERAL;
|
||||
(*instrs)[*count].literal.data = lit_data;
|
||||
(*instrs)[*count].literal.length = lit_len;
|
||||
(*count)++;
|
||||
return true;
|
||||
}
|
||||
|
||||
Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const DeltaSignature* sig,
|
||||
uint32_t block_size) {
|
||||
if (!new_file_data || !sig || new_file_size == 0 || block_size == 0)
|
||||
return NULL;
|
||||
|
||||
const uint8_t* new_data = (const uint8_t*)new_file_data;
|
||||
|
||||
uint32_t capacity = 64;
|
||||
uint32_t count = 0;
|
||||
DeltaInstruction* instrs = malloc(capacity * sizeof(DeltaInstruction));
|
||||
if (!instrs)
|
||||
return NULL;
|
||||
|
||||
uint64_t literal_start = 0;
|
||||
bool has_literal = false;
|
||||
|
||||
uint64_t i = 0;
|
||||
|
||||
uint32_t s1 = 1, s2 = 0;
|
||||
bool rolling_valid = false;
|
||||
|
||||
while (i < new_file_size) {
|
||||
uint32_t window_len =
|
||||
(uint32_t)((new_file_size - i < block_size) ? (new_file_size - i) : block_size);
|
||||
bool full_window = (window_len == block_size);
|
||||
|
||||
uint32_t adler;
|
||||
if (rolling_valid && full_window) {
|
||||
uint8_t old_byte = new_data[i - 1];
|
||||
uint8_t new_byte = new_data[i + block_size - 1];
|
||||
s1 = (s1 + DELTA_ADLER32_MODULUS - old_byte + new_byte) % DELTA_ADLER32_MODULUS;
|
||||
s2 = (s2 + DELTA_ADLER32_MODULUS -
|
||||
(uint32_t)((uint64_t)block_size * old_byte % DELTA_ADLER32_MODULUS) + s1 - 1) %
|
||||
DELTA_ADLER32_MODULUS;
|
||||
adler = (s2 << 16) | s1;
|
||||
} else {
|
||||
s1 = 1;
|
||||
s2 = 0;
|
||||
for (uint32_t k = 0; k < window_len; k++) {
|
||||
s1 = (s1 + new_data[i + k]) % DELTA_ADLER32_MODULUS;
|
||||
s2 = (s2 + s1) % DELTA_ADLER32_MODULUS;
|
||||
}
|
||||
adler = (s2 << 16) | s1;
|
||||
rolling_valid = full_window;
|
||||
}
|
||||
|
||||
bool matched = false;
|
||||
for (uint32_t j = 0; j < sig->block_count; j++) {
|
||||
if (adler == sig->blocks[j].adler32 && full_window) {
|
||||
uint32_t xxh = delta_xxhash32(new_data + i, window_len);
|
||||
if (xxh == sig->blocks[j].xxhash) {
|
||||
if (has_literal) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, i)) {
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
has_literal = false;
|
||||
}
|
||||
|
||||
if (!ensure_capacity(&instrs, &capacity, count)) {
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
instrs[count].type = DELTA_INSTR_BLOCK_MATCH;
|
||||
instrs[count].match.block_index = j;
|
||||
instrs[count].match.block_offset = 0;
|
||||
instrs[count].match.length = window_len;
|
||||
count++;
|
||||
|
||||
i += window_len;
|
||||
rolling_valid = false;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
if (!has_literal) {
|
||||
literal_start = i;
|
||||
has_literal = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (has_literal) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, new_file_size)) {
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Delta* delta = malloc(sizeof(Delta));
|
||||
if (!delta) {
|
||||
for (uint32_t k = 0; k < count; k++) {
|
||||
if (instrs[k].type == DELTA_INSTR_LITERAL)
|
||||
free(instrs[k].literal.data);
|
||||
}
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
delta->new_file_size = new_file_size;
|
||||
delta->instruction_count = count;
|
||||
delta->instructions = instrs;
|
||||
delta->delta_size = 0;
|
||||
|
||||
for (uint32_t k = 0; k < count; k++) {
|
||||
delta->delta_size += 1;
|
||||
if (instrs[k].type == DELTA_INSTR_BLOCK_MATCH) {
|
||||
delta->delta_size += sizeof(uint32_t) * 3;
|
||||
} else {
|
||||
delta->delta_size += sizeof(uint32_t) + instrs[k].literal.length;
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
Data* delta_serialize(const Delta* delta) {
|
||||
if (!delta)
|
||||
return NULL;
|
||||
|
||||
uint64_t total = sizeof(uint64_t) + sizeof(uint32_t) + delta->delta_size;
|
||||
uint8_t* buf = malloc((size_t)total);
|
||||
if (!buf)
|
||||
return NULL;
|
||||
|
||||
size_t pos = 0;
|
||||
memcpy(buf + pos, &delta->new_file_size, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
memcpy(buf + pos, &delta->instruction_count, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++) {
|
||||
uint8_t type = (uint8_t)delta->instructions[i].type;
|
||||
memcpy(buf + pos, &type, sizeof(uint8_t));
|
||||
pos += sizeof(uint8_t);
|
||||
|
||||
if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH) {
|
||||
memcpy(buf + pos, &delta->instructions[i].match.block_index, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(buf + pos, &delta->instructions[i].match.block_offset, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(buf + pos, &delta->instructions[i].match.length, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
} else {
|
||||
memcpy(buf + pos, &delta->instructions[i].literal.length, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(buf + pos, delta->instructions[i].literal.data, delta->instructions[i].literal.length);
|
||||
pos += delta->instructions[i].literal.length;
|
||||
}
|
||||
}
|
||||
|
||||
return data_create(buf, (size_t)total);
|
||||
}
|
||||
|
||||
Delta* delta_deserialize(const Data* data) {
|
||||
if (!data || data->size < sizeof(uint64_t) + sizeof(uint32_t))
|
||||
return NULL;
|
||||
|
||||
const uint8_t* buf = (const uint8_t*)data->data;
|
||||
size_t pos = 0;
|
||||
|
||||
Delta* delta = malloc(sizeof(Delta));
|
||||
if (!delta)
|
||||
return NULL;
|
||||
|
||||
memcpy(&delta->new_file_size, buf + pos, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
memcpy(&delta->instruction_count, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
|
||||
delta->instructions = malloc(delta->instruction_count * sizeof(DeltaInstruction));
|
||||
if (!delta->instructions) {
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
delta->delta_size = 0;
|
||||
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++) {
|
||||
if (pos >= data->size) {
|
||||
for (uint32_t k = 0; k < i; k++) {
|
||||
if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
||||
free(delta->instructions[k].literal.data);
|
||||
}
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t type;
|
||||
memcpy(&type, buf + pos, sizeof(uint8_t));
|
||||
pos += sizeof(uint8_t);
|
||||
|
||||
delta->delta_size += 1;
|
||||
|
||||
if (type == DELTA_OP_BLOCK_MATCH) {
|
||||
if (pos + sizeof(uint32_t) * 3 > data->size) {
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
delta->instructions[i].type = DELTA_INSTR_BLOCK_MATCH;
|
||||
memcpy(&delta->instructions[i].match.block_index, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(&delta->instructions[i].match.block_offset, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
memcpy(&delta->instructions[i].match.length, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
delta->delta_size += sizeof(uint32_t) * 3;
|
||||
} else if (type == DELTA_OP_LITERAL) {
|
||||
if (pos + sizeof(uint32_t) > data->size) {
|
||||
for (uint32_t k = 0; k < i; k++) {
|
||||
if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
||||
free(delta->instructions[k].literal.data);
|
||||
}
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
delta->instructions[i].type = DELTA_INSTR_LITERAL;
|
||||
memcpy(&delta->instructions[i].literal.length, buf + pos, sizeof(uint32_t));
|
||||
pos += sizeof(uint32_t);
|
||||
|
||||
uint32_t lit_len = delta->instructions[i].literal.length;
|
||||
if (pos + lit_len > data->size) {
|
||||
for (uint32_t k = 0; k < i; k++) {
|
||||
if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
||||
free(delta->instructions[k].literal.data);
|
||||
}
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
delta->instructions[i].literal.data = malloc(lit_len);
|
||||
if (!delta->instructions[i].literal.data) {
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(delta->instructions[i].literal.data, buf + pos, lit_len);
|
||||
pos += lit_len;
|
||||
delta->delta_size += sizeof(uint32_t) + lit_len;
|
||||
} else {
|
||||
for (uint32_t k = 0; k < i; k++) {
|
||||
if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
||||
free(delta->instructions[k].literal.data);
|
||||
}
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta,
|
||||
uint32_t block_size) {
|
||||
if (!old_data || !delta)
|
||||
return NULL;
|
||||
|
||||
void* output = malloc((size_t)delta->new_file_size);
|
||||
if (!output)
|
||||
return NULL;
|
||||
|
||||
uint8_t* out = (uint8_t*)output;
|
||||
const uint8_t* old = (const uint8_t*)old_data;
|
||||
uint64_t out_pos = 0;
|
||||
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++) {
|
||||
if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH) {
|
||||
uint64_t src_offset = (uint64_t)delta->instructions[i].match.block_index * block_size;
|
||||
src_offset += delta->instructions[i].match.block_offset;
|
||||
uint32_t len = delta->instructions[i].match.length;
|
||||
|
||||
if (src_offset + len > old_size) {
|
||||
free(output);
|
||||
return NULL;
|
||||
}
|
||||
memcpy(out + out_pos, old + src_offset, len);
|
||||
out_pos += len;
|
||||
} else {
|
||||
uint32_t len = delta->instructions[i].literal.length;
|
||||
memcpy(out + out_pos, delta->instructions[i].literal.data, len);
|
||||
out_pos += len;
|
||||
}
|
||||
}
|
||||
|
||||
if (out_pos != delta->new_file_size) {
|
||||
free(output);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void delta_destroy(Delta* delta) {
|
||||
if (!delta)
|
||||
return;
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++) {
|
||||
if (delta->instructions[i].type == DELTA_INSTR_LITERAL)
|
||||
free(delta->instructions[i].literal.data);
|
||||
}
|
||||
free(delta->instructions);
|
||||
free(delta);
|
||||
}
|
||||
|
||||
bool delta_should_attempt(uint64_t old_size, uint64_t new_size, uint64_t max_file_size) {
|
||||
if (old_size < DELTA_MIN_FILE_SIZE || new_size < DELTA_MIN_FILE_SIZE)
|
||||
return false;
|
||||
if (old_size > max_file_size || new_size > max_file_size)
|
||||
return false;
|
||||
double large = (old_size > new_size) ? (double)old_size : (double)new_size;
|
||||
double small = (old_size > new_size) ? (double)new_size : (double)old_size;
|
||||
if (small == 0 || large / small > DELTA_MAX_SIZE_RATIO)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size) {
|
||||
if (!delta || delta->instruction_count == 0)
|
||||
return false;
|
||||
|
||||
bool has_match = false;
|
||||
for (uint32_t i = 0; i < delta->instruction_count; i++) {
|
||||
if (delta->instructions[i].type == DELTA_INSTR_BLOCK_MATCH) {
|
||||
has_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_match)
|
||||
return false;
|
||||
|
||||
double ratio = (double)delta->delta_size / (double)new_file_size;
|
||||
return ratio < DELTA_FALLBACK_RATIO;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
#ifndef DELTA_H
|
||||
#define DELTA_H
|
||||
|
||||
#include "data.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define DELTA_BLOCK_SIZE_DEFAULT 8192U
|
||||
#define DELTA_BLOCK_SIZE_MIN 1024U
|
||||
#define DELTA_BLOCK_SIZE_MAX 65536U
|
||||
#define DELTA_MIN_FILE_SIZE 16384ULL
|
||||
#define DELTA_MAX_FILE_SIZE (256ULL * 1024 * 1024)
|
||||
#define DELTA_MAX_SIZE_RATIO 10.0
|
||||
#define DELTA_FALLBACK_RATIO 0.7
|
||||
#define DELTA_ADLER32_MODULUS 65521U
|
||||
|
||||
#define DELTA_OP_BLOCK_MATCH 0x01
|
||||
#define DELTA_OP_LITERAL 0x02
|
||||
|
||||
typedef struct {
|
||||
uint32_t adler32;
|
||||
uint32_t xxhash;
|
||||
} DeltaBlockSig;
|
||||
|
||||
typedef struct {
|
||||
uint64_t file_size;
|
||||
uint32_t block_size;
|
||||
uint32_t block_count;
|
||||
DeltaBlockSig* blocks;
|
||||
} DeltaSignature;
|
||||
|
||||
typedef enum { DELTA_INSTR_BLOCK_MATCH = 0x01, DELTA_INSTR_LITERAL = 0x02 } DeltaInstrType;
|
||||
|
||||
typedef struct {
|
||||
DeltaInstrType type;
|
||||
union {
|
||||
struct {
|
||||
uint32_t block_index;
|
||||
uint32_t block_offset;
|
||||
uint32_t length;
|
||||
} match;
|
||||
struct {
|
||||
uint8_t* data;
|
||||
uint32_t length;
|
||||
} literal;
|
||||
};
|
||||
} DeltaInstruction;
|
||||
|
||||
typedef struct {
|
||||
uint64_t new_file_size;
|
||||
uint32_t instruction_count;
|
||||
DeltaInstruction* instructions;
|
||||
uint64_t delta_size;
|
||||
} Delta;
|
||||
|
||||
DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_file_size,
|
||||
uint32_t block_size);
|
||||
Data* delta_signature_serialize(const DeltaSignature* sig);
|
||||
DeltaSignature* delta_signature_deserialize(const Data* data);
|
||||
void delta_signature_destroy(DeltaSignature* sig);
|
||||
|
||||
Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const DeltaSignature* sig,
|
||||
uint32_t block_size);
|
||||
Data* delta_serialize(const Delta* delta);
|
||||
Delta* delta_deserialize(const Data* data);
|
||||
void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta, uint32_t block_size);
|
||||
void delta_destroy(Delta* delta);
|
||||
|
||||
bool delta_should_attempt(uint64_t old_size, uint64_t new_size, uint64_t max_file_size);
|
||||
bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size);
|
||||
|
||||
uint32_t delta_adler32(const void* data, uint32_t len);
|
||||
uint32_t delta_xxhash32(const void* data, uint32_t len);
|
||||
|
||||
#endif
|
||||
+643
-44
@@ -1,35 +1,52 @@
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <zstd.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "compression.h"
|
||||
#include "delta.h"
|
||||
#include "log.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "socket.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
|
||||
File *file_create(const char *path, struct stat *stats) {
|
||||
#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 = (File*)malloc(sizeof(File));
|
||||
if (file == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for file struct");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("ERROR: Could not allocate memory for file struct");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
file->stats = *stats;
|
||||
|
||||
int path_len = strlen(path);
|
||||
file->path = (char*)malloc(path_len + 1);
|
||||
if (file->path == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for path file string");
|
||||
free(file);
|
||||
exit(EXIT_FAILURE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
strcpy(file->path, path);
|
||||
file->data = NULL;
|
||||
file->data = data_create_reserve(0);
|
||||
if (file->data == NULL) {
|
||||
free(file->path);
|
||||
free(file);
|
||||
return NULL;
|
||||
}
|
||||
file->metadata = NULL;
|
||||
file->type = FILE_TYPE_REGULAR;
|
||||
file->link_target = NULL;
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -37,35 +54,615 @@ void file_destroy(void *item) {
|
||||
if (item == NULL)
|
||||
return;
|
||||
File* file = (File*)item;
|
||||
free(file->data);
|
||||
data_destroy(file->data);
|
||||
file->data = NULL;
|
||||
file_metadata_destroy(file->metadata);
|
||||
file->metadata = NULL;
|
||||
free(file->path);
|
||||
file->path = NULL;
|
||||
free(file->link_target);
|
||||
file->link_target = NULL;
|
||||
free(file);
|
||||
}
|
||||
|
||||
void file_load_data(File *file) {
|
||||
FileMetadata* file_metadata_create(const struct stat* stats) {
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL) {
|
||||
perror("ERROR: Could not allocate memory for file metadata");
|
||||
return NULL;
|
||||
}
|
||||
m->mode = stats->st_mode;
|
||||
m->uid = stats->st_uid;
|
||||
m->gid = stats->st_gid;
|
||||
m->mtime_sec = stats->st_mtime;
|
||||
#ifdef __linux__
|
||||
m->mtime_nsec = stats->st_mtim.tv_nsec;
|
||||
#else
|
||||
m->mtime_nsec = 0;
|
||||
#endif
|
||||
return m;
|
||||
}
|
||||
|
||||
void file_metadata_destroy(void* metadata) {
|
||||
free(metadata);
|
||||
}
|
||||
|
||||
bool file_load_data(File* file) {
|
||||
if (file == NULL)
|
||||
return;
|
||||
file->data = data_create_empty(file->stats.st_size);
|
||||
printf("%ld is file big", file->data->size);
|
||||
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) {
|
||||
file->data->data = malloc(file->data->size);
|
||||
if (file->data->data == NULL) {
|
||||
perror("Could not allocate memory for file data");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
size_t bytes_read = file_content_to_buffer(file);
|
||||
if (bytes_read != file->data->size) {
|
||||
log_message(STATUS_ERROR, "Didnt read expected amount of bytes from file");
|
||||
exit(EXIT_FAILURE);
|
||||
log_message(LOG_LEVEL_ERROR, "Did not read expected amount of bytes from file");
|
||||
return false;
|
||||
}
|
||||
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,
|
||||
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;
|
||||
Data* compressed_data = NULL;
|
||||
|
||||
// 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);
|
||||
if (compressed_data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to compress file data");
|
||||
return false;
|
||||
}
|
||||
data_to_send = compressed_data;
|
||||
}
|
||||
if (!send_data(file_descriptor, data_to_send)) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
}
|
||||
data_destroy(compressed_data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool file_save_to_disk(const char* root_directory, File* file) {
|
||||
if (file->type == FILE_TYPE_SYMLINK && file->link_target) {
|
||||
char* disk_path = path_cat((char*)root_directory, file->path);
|
||||
if (disk_path == NULL)
|
||||
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);
|
||||
if (disk_path == NULL)
|
||||
return false;
|
||||
bool ok = to_disk(disk_path, file->data->data, file->data->size);
|
||||
if (ok)
|
||||
file_restore_metadata(disk_path, file->metadata);
|
||||
free(disk_path);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void* old_data_from_path(const char* full_path, unsigned long long old_size) {
|
||||
void* data = malloc((size_t)old_size);
|
||||
if (!data)
|
||||
return NULL;
|
||||
FILE* fp = fopen(full_path, "rb");
|
||||
if (!fp) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
size_t nread = fread(data, 1, (size_t)old_size, fp);
|
||||
fclose(fp);
|
||||
if (nread != (size_t)old_size) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: receive data from wire, optionally decompress, and store in file.
|
||||
* On success, returns the received Data* (caller owns it). On failure, returns NULL.
|
||||
* If `file_data` is received via receive_data(fd), this function handles decompression
|
||||
* when config->use_compression is set.
|
||||
*/
|
||||
static Data* receive_and_decompress(int fd, const Config* config) {
|
||||
Data* file_data = receive_data(fd);
|
||||
if (file_data == NULL)
|
||||
return NULL;
|
||||
if (config->use_compression) {
|
||||
Data* uncompressed = data_decompress(file_data);
|
||||
data_destroy(file_data);
|
||||
if (uncompressed == NULL)
|
||||
return NULL;
|
||||
file_data = uncompressed;
|
||||
}
|
||||
return file_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: receive metadata from wire and assign to file.
|
||||
* Returns true on success (metadata may be NULL if absent), false on I/O error.
|
||||
*/
|
||||
static bool receive_and_assign_metadata(int fd, const Config* config, File* file) {
|
||||
if (!config->use_metadata)
|
||||
return true;
|
||||
int meta_ok = 1;
|
||||
file->metadata = metadata_receive(fd, &meta_ok);
|
||||
if (!meta_ok) {
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static File* receive_delta_file(int fd, const Config* config, const char* check_path,
|
||||
void* old_data, unsigned long long old_size) {
|
||||
if (!old_data)
|
||||
return NULL;
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, old_size, config->delta_block_size);
|
||||
if (!sig) {
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* sig_data = delta_signature_serialize(sig);
|
||||
if (!sig_data) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data);
|
||||
data_destroy(sig_data);
|
||||
|
||||
if (!sig_sent) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Status resp;
|
||||
if (!receive_status(fd, &resp)) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (resp == STATUS_DELTA_DATA) {
|
||||
Data* delta_data = receive_data(fd);
|
||||
if (!delta_data) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* raw_delta = delta_data;
|
||||
if (config->use_compression) {
|
||||
raw_delta = data_decompress(delta_data);
|
||||
data_destroy(delta_data);
|
||||
if (!raw_delta) {
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void file_print(void *item) {
|
||||
if (item == NULL)
|
||||
return;
|
||||
printf("%s\n", ((File *)item)->path);
|
||||
Delta* delta = delta_deserialize(raw_delta);
|
||||
data_destroy(raw_delta);
|
||||
if (!delta) {
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void file_send_single_calls(File *file, int file_descriptor) {
|
||||
send_str(file_descriptor, file->path);
|
||||
printf("Sending File: %ld", file->data->size);
|
||||
send_data(file_descriptor, file->data->data, file->data->size);
|
||||
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size);
|
||||
uint64_t new_size = delta->new_file_size;
|
||||
delta_destroy(delta);
|
||||
|
||||
if (!new_data) {
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
File* file = file_create(check_path);
|
||||
if (!file) {
|
||||
free(new_data);
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (config->use_metadata) {
|
||||
int meta_ok = 1;
|
||||
file->metadata = metadata_receive(fd, &meta_ok);
|
||||
if (!meta_ok) {
|
||||
file_destroy(file);
|
||||
free(new_data);
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = data_create(new_data, (size_t)new_size);
|
||||
|
||||
free(old_data);
|
||||
delta_signature_destroy(sig);
|
||||
return file;
|
||||
}
|
||||
|
||||
if (resp == STATUS_NEXT) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
|
||||
File* file = file_create(check_path);
|
||||
if (!file) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!receive_and_assign_metadata(fd, config, file))
|
||||
return NULL;
|
||||
|
||||
Data* file_data = receive_and_decompress(fd, config);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
}
|
||||
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
*skipped = false;
|
||||
char* check_path = receive_str(fd);
|
||||
if (check_path == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
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);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* full_path = path_cat(config->receive_root_directory, check_path);
|
||||
struct stat st;
|
||||
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;
|
||||
|
||||
// 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 &&
|
||||
(long long)st.st_mtime == check_mtime;
|
||||
|
||||
if (match) {
|
||||
if (!send_status(fd, STATUS_OK)) {
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
return NULL;
|
||||
}
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
*skipped = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool try_delta = config->use_delta && has_old_file &&
|
||||
delta_should_attempt(old_size, check_size, config->delta_max_file_size);
|
||||
|
||||
if (try_delta) {
|
||||
void* old_data = old_data_from_path(full_path, old_size);
|
||||
File* delta_file = receive_delta_file(fd, config, check_path, old_data, old_size);
|
||||
if (delta_file) {
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
return delta_file;
|
||||
}
|
||||
try_delta = false;
|
||||
}
|
||||
|
||||
if (!try_delta) {
|
||||
if (!send_status(fd, STATUS_NEXT)) {
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
File* file = file_create(check_path);
|
||||
free(check_path);
|
||||
free(full_path);
|
||||
if (file == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!receive_and_assign_metadata(fd, config, file))
|
||||
return NULL;
|
||||
|
||||
// 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_and_decompress(fd, config);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
}
|
||||
|
||||
bool to_disk(const char* path, const void* data, unsigned long long data_size) {
|
||||
// dirname() may modify its argument and may return a pointer to static storage.
|
||||
// We must use a copy of the result to be safe.
|
||||
char* path_dup = str_dup(path);
|
||||
if (!path_dup)
|
||||
return false;
|
||||
const char* dir_result = dirname(path_dup);
|
||||
char* directory = str_dup(dir_result);
|
||||
free(path_dup);
|
||||
if (!directory)
|
||||
return false;
|
||||
|
||||
bool ok = true;
|
||||
if (!mkdir_r(directory)) {
|
||||
ok = false;
|
||||
goto done;
|
||||
}
|
||||
FILE* file_pointer = fopen(path, "wb");
|
||||
if (file_pointer == NULL) {
|
||||
perror("Could not open File");
|
||||
ok = false;
|
||||
goto done;
|
||||
}
|
||||
if (fwrite(data, 1, data_size, file_pointer) != data_size) {
|
||||
perror("Failed to write all data to disk");
|
||||
fclose(file_pointer);
|
||||
ok = false;
|
||||
goto done;
|
||||
}
|
||||
fclose(file_pointer);
|
||||
|
||||
done:
|
||||
free(directory);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level,
|
||||
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).
|
||||
// If compression is requested, fall back to the regular send path.
|
||||
if (compression_level > 0)
|
||||
return file_send_single_calls(file, file_descriptor, use_metadata, compression_level,
|
||||
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
|
||||
int ft = (int)file->type;
|
||||
if (!send_int(file_descriptor, ft))
|
||||
return false;
|
||||
|
||||
int fd = open(file->path, O_RDONLY);
|
||||
if (fd == -1) {
|
||||
perror("Could not open file for sendfile");
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned long long file_size = file->data->size;
|
||||
if (!send_n_data(file_descriptor, &file_size, sizeof(unsigned long long))) {
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
|
||||
off_t offset = 0;
|
||||
while ((unsigned long long)offset < file_size) {
|
||||
ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset);
|
||||
if (sent == -1) {
|
||||
perror("sendfile failed");
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
File* file_receive(const Config* config, int file_descriptor) {
|
||||
char* path = receive_str(file_descriptor);
|
||||
if (path == NULL)
|
||||
return NULL;
|
||||
File* file = file_create(path);
|
||||
free(path);
|
||||
if (file == NULL)
|
||||
return NULL;
|
||||
if (config->use_metadata) {
|
||||
int meta_ok = 1;
|
||||
file->metadata = metadata_receive(file_descriptor, &meta_ok);
|
||||
if (!meta_ok) {
|
||||
file_destroy(file);
|
||||
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);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
if (config->use_compression) {
|
||||
Data* file_data_uncompressed = data_decompress(file_data);
|
||||
data_destroy(file_data);
|
||||
if (file_data_uncompressed == NULL) {
|
||||
file_destroy(file);
|
||||
return NULL;
|
||||
}
|
||||
file_data = file_data_uncompressed;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
}
|
||||
|
||||
size_t file_content_to_buffer(File* file) {
|
||||
@@ -74,30 +671,32 @@ size_t file_content_to_buffer(File *file) {
|
||||
perror("Could not open the file!");
|
||||
return 0;
|
||||
}
|
||||
size_t bytes_read =
|
||||
fread(file->data->data, 1, file->stats.st_size, file_pointer);
|
||||
if (bytes_read != (size_t)file->stats.st_size) {
|
||||
perror("Read to many or to less bytes from File!");
|
||||
size_t bytes_read = fread(file->data->data, 1, file->data->size, file_pointer);
|
||||
if (bytes_read != (size_t)file->data->size) {
|
||||
fclose(file_pointer);
|
||||
perror("Read unexpected number of bytes from File!");
|
||||
return 0;
|
||||
}
|
||||
fclose(file_pointer);
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
FileReceive *file_receive_create(char *path, Data *data) {
|
||||
FileReceive *file = malloc(sizeof(FileReceive));
|
||||
file->path = path;
|
||||
file->data = data;
|
||||
return file;
|
||||
int receive_manifest(int fd, const Config* config, int* next_status) {
|
||||
int count;
|
||||
if (!receive_int(fd, &count))
|
||||
return -1;
|
||||
ArrayList* manifest = array_list_create(free);
|
||||
if (manifest) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
char* s = receive_str(fd);
|
||||
if (s)
|
||||
array_list_add(manifest, s);
|
||||
}
|
||||
|
||||
void file_receive_destroy(void *file_receive) {
|
||||
if (file_receive == NULL)
|
||||
return;
|
||||
FileReceive *file = (FileReceive *)file_receive;
|
||||
data_destroy(file->data);
|
||||
free(file->path);
|
||||
free(file);
|
||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||
delete_extras(config->receive_root_directory, manifest);
|
||||
array_list_delete(manifest);
|
||||
}
|
||||
if (!receive_status(fd, next_status))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
FileReceive *file_receive_from_buffer(void *buffer) {}
|
||||
|
||||
+28
-16
@@ -1,31 +1,43 @@
|
||||
#ifndef FILE_H
|
||||
#define FILE_H
|
||||
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include <stdbool.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
typedef enum { FILE_TYPE_REGULAR, FILE_TYPE_SYMLINK, FILE_TYPE_DIR } FileType;
|
||||
|
||||
typedef struct {
|
||||
mode_t mode;
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
time_t mtime_sec;
|
||||
long mtime_nsec;
|
||||
} FileMetadata;
|
||||
|
||||
typedef struct {
|
||||
char* path;
|
||||
struct stat stats;
|
||||
Data* data;
|
||||
FileMetadata* metadata;
|
||||
FileType type;
|
||||
char* link_target;
|
||||
} File;
|
||||
|
||||
typedef struct {
|
||||
char *path;
|
||||
Data *data;
|
||||
} FileReceive;
|
||||
|
||||
File *file_create(const char *path, struct stat *stats);
|
||||
File* file_create(const char* path);
|
||||
void file_destroy(void* item);
|
||||
void file_load_data(File *file);
|
||||
void file_print(void *item);
|
||||
void file_send_single_calls(File *file, int file_descriptor);
|
||||
bool file_load_data(File* file);
|
||||
File* file_receive(const Config* config, int file_descriptor);
|
||||
bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||
int compression_level, bool send_path);
|
||||
bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int compression_level,
|
||||
bool send_path);
|
||||
size_t file_content_to_buffer(File* file);
|
||||
Data *file_compress(File *file);
|
||||
|
||||
FileReceive *file_receive_create(char *path, Data *data);
|
||||
void file_receive_destroy(void *file_receive);
|
||||
FileReceive *file_receive_from_buffer(void *buffer);
|
||||
FileReceive *file_receive_decompress(void *FileReceive);
|
||||
FileMetadata* file_metadata_create(const struct stat* stats);
|
||||
void file_metadata_destroy(void* metadata);
|
||||
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);
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
||||
int receive_manifest(int fd, const Config* config, int* next_status);
|
||||
|
||||
#endif
|
||||
|
||||
+11
-9
@@ -4,22 +4,24 @@
|
||||
#include <time.h>
|
||||
|
||||
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
||||
static LogLevel current_log_level = LOG_LEVEL_DEBUG;
|
||||
static LogLevel current_log_level = LOG_LEVEL_WARNING;
|
||||
|
||||
void log_message(LogLevel log_level, char *format, ...) {
|
||||
void set_log_level(LogLevel level) {
|
||||
current_log_level = level;
|
||||
}
|
||||
|
||||
void log_message(LogLevel log_level, const char* format, ...) {
|
||||
if (log_level < current_log_level)
|
||||
return;
|
||||
time_t now = time(NULL);
|
||||
struct tm *t = localtime(&now);
|
||||
const struct tm* t = localtime(&now);
|
||||
|
||||
// Print timestamp and log level to the file
|
||||
printf("%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]);
|
||||
fprintf(stderr, "%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_list args;
|
||||
va_start(args, format);
|
||||
vprintf(format, args);
|
||||
vfprintf(stderr, format, args);
|
||||
va_end(args);
|
||||
printf("\n");
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
+3
-7
@@ -1,13 +1,9 @@
|
||||
#ifndef LOG_H
|
||||
#define LOG_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, char *message, ...);
|
||||
void log_message(LogLevel log_level, const char* message, ...);
|
||||
void set_log_level(LogLevel level);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
#include "metadata.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void metadata_to_buf(char** buf, const FileMetadata* m) {
|
||||
int32_t present = (m != NULL) ? 1 : 0;
|
||||
memcpy(*buf, &present, sizeof(present));
|
||||
*buf += sizeof(present);
|
||||
if (m == NULL)
|
||||
return;
|
||||
int32_t mode = (int32_t)m->mode;
|
||||
memcpy(*buf, &mode, sizeof(mode));
|
||||
*buf += sizeof(mode);
|
||||
int32_t uid = (int32_t)m->uid;
|
||||
memcpy(*buf, &uid, sizeof(uid));
|
||||
*buf += sizeof(uid);
|
||||
int32_t gid = (int32_t)m->gid;
|
||||
memcpy(*buf, &gid, sizeof(gid));
|
||||
*buf += sizeof(gid);
|
||||
int64_t mtime_sec = (int64_t)m->mtime_sec;
|
||||
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) {
|
||||
int32_t present;
|
||||
memcpy(&present, *buf, sizeof(present));
|
||||
*buf += sizeof(present);
|
||||
if (!present)
|
||||
return NULL;
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
int32_t mode;
|
||||
memcpy(&mode, *buf, sizeof(mode));
|
||||
*buf += sizeof(mode);
|
||||
m->mode = (mode_t)mode;
|
||||
int32_t uid;
|
||||
memcpy(&uid, *buf, sizeof(uid));
|
||||
*buf += sizeof(uid);
|
||||
m->uid = (uid_t)uid;
|
||||
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;
|
||||
}
|
||||
|
||||
bool metadata_send(int file_descriptor, FileMetadata* m) {
|
||||
if (m == NULL) {
|
||||
int32_t zero = 0;
|
||||
return send_n_data(file_descriptor, &zero, sizeof(zero));
|
||||
}
|
||||
int32_t present = 1;
|
||||
int32_t mode = (int32_t)m->mode;
|
||||
int32_t uid = (int32_t)m->uid;
|
||||
int32_t gid = (int32_t)m->gid;
|
||||
int64_t mtime_sec = (int64_t)m->mtime_sec;
|
||||
int64_t mtime_nsec = (int64_t)m->mtime_nsec;
|
||||
return send_n_data(file_descriptor, &present, sizeof(present)) &&
|
||||
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) {
|
||||
int32_t present;
|
||||
if (!receive_n_data(file_descriptor, &present, sizeof(present))) {
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
return NULL;
|
||||
}
|
||||
if (!present) {
|
||||
if (ok)
|
||||
*ok = 1;
|
||||
return NULL;
|
||||
}
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL) {
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
return NULL;
|
||||
}
|
||||
int32_t mode;
|
||||
if (!receive_n_data(file_descriptor, &mode, sizeof(mode))) {
|
||||
free(m);
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
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)
|
||||
*ok = 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
void file_restore_metadata(const char* path, FileMetadata* metadata) {
|
||||
if (metadata == NULL)
|
||||
return;
|
||||
if (chmod(path, metadata->mode & 07777) != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to chmod %s: %s", path, strerror(errno));
|
||||
if (chown(path, metadata->uid, metadata->gid) != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to chown %s: %s", path, strerror(errno));
|
||||
struct timespec times[2];
|
||||
times[0].tv_sec = 0;
|
||||
times[0].tv_nsec = UTIME_OMIT;
|
||||
times[1].tv_sec = metadata->mtime_sec;
|
||||
times[1].tv_nsec = metadata->mtime_nsec;
|
||||
if (utimensat(AT_FDCWD, path, times, 0) != 0)
|
||||
log_message(LOG_LEVEL_WARNING, "Failed to set timestamps on %s: %s", path, strerror(errno));
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef METADATA_H
|
||||
#define METADATA_H
|
||||
|
||||
#include "file.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define FILE_METADATA_WIRE_SIZE (sizeof(int32_t) * 3 + sizeof(int64_t) * 2)
|
||||
|
||||
void metadata_to_buf(char** buf, const FileMetadata* m);
|
||||
FileMetadata* metadata_from_buf(char** buf);
|
||||
bool metadata_send(int file_descriptor, FileMetadata* m);
|
||||
FileMetadata* metadata_receive(int file_descriptor, int* ok);
|
||||
void file_restore_metadata(const char* path, FileMetadata* metadata);
|
||||
|
||||
#endif
|
||||
@@ -1,32 +1,46 @@
|
||||
#include "multiprocessing.h"
|
||||
#include "array_list.h"
|
||||
#include "chunk.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "queue.h"
|
||||
#include "utils.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <threads.h>
|
||||
|
||||
PipelineContextSender *pipeline_context_sender_create(Config *config,
|
||||
Queue *queue_scanner,
|
||||
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
|
||||
Queue* queue_loader) {
|
||||
PipelineContextSender* context = malloc(sizeof(PipelineContextSender));
|
||||
if (context == NULL)
|
||||
return NULL;
|
||||
context->config = config;
|
||||
context->queue_scanner = queue_scanner;
|
||||
context->queue_loader = queue_loader;
|
||||
context->scanner_done = false;
|
||||
context->loader_done = false;
|
||||
context->manifest = NULL;
|
||||
if (mtx_init(&context->mutex_scanner, mtx_plain) != thrd_success ||
|
||||
cnd_init(&context->condition_not_full_scanner) != thrd_success ||
|
||||
cnd_init(&context->condition_not_empty_scanner) != thrd_success ||
|
||||
mtx_init(&context->mutex_loader, mtx_plain) != thrd_success ||
|
||||
cnd_init(&context->condition_not_full_loader) != thrd_success ||
|
||||
cnd_init(&context->condition_not_empty_loader) != thrd_success) {
|
||||
perror("Error initializing synchronization objects!");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("Error initializing synchronization objects");
|
||||
free(context);
|
||||
return NULL;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
void pipeline_context_sender_destroy(PipelineContextSender* context) {
|
||||
if (context->manifest) {
|
||||
array_list_delete(context->manifest);
|
||||
}
|
||||
config_delete(context->config);
|
||||
queue_destroy(context->queue_scanner);
|
||||
queue_destroy(context->queue_loader);
|
||||
@@ -39,10 +53,11 @@ void pipeline_context_sender_destroy(PipelineContextSender *context) {
|
||||
free(context);
|
||||
}
|
||||
|
||||
PipelineContextReceiver *pipeline_context_receiver_create(Config *config,
|
||||
Queue *queue,
|
||||
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue,
|
||||
int file_descriptor) {
|
||||
PipelineContextReceiver* context = malloc(sizeof(PipelineContextReceiver));
|
||||
if (context == NULL)
|
||||
return NULL;
|
||||
context->config = config;
|
||||
context->queue = queue;
|
||||
context->file_descriptor = file_descriptor;
|
||||
@@ -50,8 +65,9 @@ PipelineContextReceiver *pipeline_context_receiver_create(Config *config,
|
||||
if (mtx_init(&context->mutex, mtx_plain) != thrd_success ||
|
||||
cnd_init(&context->condition_not_full) != thrd_success ||
|
||||
cnd_init(&context->condition_not_empty) != thrd_success) {
|
||||
perror("Error initializing synchronization objects!");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("Error initializing synchronization objects");
|
||||
free(context);
|
||||
return NULL;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -64,3 +80,83 @@ void pipeline_context_receiver_destroy(PipelineContextReceiver *context) {
|
||||
cnd_destroy(&context->condition_not_empty);
|
||||
free(context);
|
||||
}
|
||||
|
||||
static void receive_chunk_enqueue(int file_descriptor, PipelineContextReceiver* context) {
|
||||
Chunk* chunk = receive_chunk_data(file_descriptor, context->config);
|
||||
if (chunk == NULL)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
File* file = chunk->items[i];
|
||||
chunk->items[i] = NULL;
|
||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty, &context->condition_not_full);
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
}
|
||||
|
||||
int receive_thread(void* pipeline_context) {
|
||||
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
||||
mtx_lock(&context->mutex);
|
||||
int file_descriptor = context->file_descriptor;
|
||||
const Config* config = context->config;
|
||||
mtx_unlock(&context->mutex);
|
||||
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return thrd_error;
|
||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK) {
|
||||
if (status == STATUS_CHECK) {
|
||||
bool skipped;
|
||||
File* file = receive_incremental_check(file_descriptor, config, &skipped);
|
||||
if (!skipped) {
|
||||
if (file == NULL)
|
||||
return thrd_error;
|
||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty, &context->condition_not_full);
|
||||
}
|
||||
} else if (status == STATUS_CHUNK) {
|
||||
receive_chunk_enqueue(file_descriptor, context);
|
||||
} else {
|
||||
File* file = file_receive(config, file_descriptor);
|
||||
if (file) {
|
||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty, &context->condition_not_full);
|
||||
} else {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
||||
}
|
||||
}
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return thrd_error;
|
||||
}
|
||||
if (status == STATUS_MANIFEST) {
|
||||
if (receive_manifest(file_descriptor, config, &status) != 0)
|
||||
return thrd_error;
|
||||
}
|
||||
mtx_lock(&context->mutex);
|
||||
context->receiver_done = true;
|
||||
cnd_signal(&context->condition_not_empty);
|
||||
mtx_unlock(&context->mutex);
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
int write_thread(void* pipeline_context) {
|
||||
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
||||
mtx_lock(&context->mutex);
|
||||
bool save_to_disk = context->config->save_to_disk;
|
||||
char* root_directory = str_dup(context->config->receive_root_directory);
|
||||
mtx_unlock(&context->mutex);
|
||||
|
||||
while (true) {
|
||||
File* file =
|
||||
queue_dequeue_multithreaded(context->queue, &context->mutex, &context->condition_not_empty,
|
||||
&context->condition_not_full, &context->receiver_done);
|
||||
if (file == NULL) {
|
||||
free(root_directory);
|
||||
return thrd_success;
|
||||
}
|
||||
if (save_to_disk)
|
||||
file_save_to_disk(root_directory, file);
|
||||
file_destroy(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <threads.h>
|
||||
|
||||
#include "array_list.h"
|
||||
#include "config.h"
|
||||
#include "file.h"
|
||||
#include "queue.h"
|
||||
|
||||
typedef struct {
|
||||
@@ -18,6 +20,7 @@ typedef struct {
|
||||
cnd_t condition_not_full_loader;
|
||||
cnd_t condition_not_empty_loader;
|
||||
bool loader_done;
|
||||
ArrayList* manifest;
|
||||
} PipelineContextSender;
|
||||
|
||||
typedef struct PipelineContextReceiver {
|
||||
@@ -30,12 +33,12 @@ typedef struct PipelineContextReceiver {
|
||||
bool receiver_done;
|
||||
} PipelineContextReceiver;
|
||||
|
||||
PipelineContextSender *pipeline_context_sender_create(Config *config,
|
||||
Queue *queue_scanner,
|
||||
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
|
||||
Queue* queue_loader);
|
||||
void pipeline_context_sender_destroy(PipelineContextSender* context);
|
||||
PipelineContextReceiver *pipeline_context_receiver_create(Config *config,
|
||||
Queue *queue_receiver,
|
||||
PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue* queue_receiver,
|
||||
int file_descriptor);
|
||||
void pipeline_context_receiver_destroy(PipelineContextReceiver* context);
|
||||
int receive_thread(void* pipeline_context);
|
||||
int write_thread(void* pipeline_context);
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "protocol.h"
|
||||
#include "log.h"
|
||||
#include <errno.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static __thread int io_read_fd = -1;
|
||||
static __thread int io_write_fd = -1;
|
||||
static SSL* io_ssl = NULL;
|
||||
|
||||
static unsigned long long io_bwlimit = 0;
|
||||
static long long bw_tokens = 0;
|
||||
static struct timespec bw_last_refill = {0, 0};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd) {
|
||||
io_read_fd = read_fd;
|
||||
io_write_fd = write_fd;
|
||||
}
|
||||
|
||||
void io_set_bwlimit(unsigned long long bytes_per_sec) {
|
||||
io_bwlimit = bytes_per_sec;
|
||||
bw_tokens = (long long)io_bwlimit;
|
||||
clock_gettime(CLOCK_MONOTONIC, &bw_last_refill);
|
||||
}
|
||||
|
||||
static void bw_throttle(size_t bytes_written) {
|
||||
if (io_bwlimit == 0)
|
||||
return;
|
||||
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
|
||||
/* Use unsigned long long for elapsed_ns to avoid overflow in multiplication.
|
||||
* time_t differences fit comfortably in 64-bit for any practical runtime. */
|
||||
unsigned long long elapsed_ns =
|
||||
(unsigned long long)(now.tv_sec - bw_last_refill.tv_sec) * 1000000000ULL +
|
||||
(unsigned long long)(now.tv_nsec - bw_last_refill.tv_nsec);
|
||||
bw_last_refill = now;
|
||||
|
||||
long long tokens_to_add = (long long)((double)io_bwlimit * (double)elapsed_ns / 1000000000.0);
|
||||
bw_tokens += tokens_to_add;
|
||||
if (bw_tokens > (long long)io_bwlimit)
|
||||
bw_tokens = (long long)io_bwlimit;
|
||||
|
||||
bw_tokens -= (long long)bytes_written;
|
||||
|
||||
if (bw_tokens < 0) {
|
||||
long long deficit_ns = (long long)((double)(-bw_tokens) / (double)io_bwlimit * 1000000000.0);
|
||||
if (deficit_ns < 0)
|
||||
deficit_ns = 0;
|
||||
struct timespec sleep_time, remaining;
|
||||
sleep_time.tv_sec = deficit_ns / 1000000000LL;
|
||||
sleep_time.tv_nsec = deficit_ns % 1000000000LL;
|
||||
while (nanosleep(&sleep_time, &remaining) < 0 && errno == EINTR)
|
||||
sleep_time = remaining;
|
||||
bw_tokens = 0;
|
||||
clock_gettime(CLOCK_MONOTONIC, &bw_last_refill);
|
||||
}
|
||||
}
|
||||
|
||||
void io_set_ssl(SSL* ssl) {
|
||||
io_ssl = ssl;
|
||||
}
|
||||
|
||||
static int io_fd(int dir_fd, int file_descriptor) {
|
||||
return (dir_fd != -1) ? dir_fd : file_descriptor;
|
||||
}
|
||||
|
||||
bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
||||
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %zu", data_size);
|
||||
int fd = io_fd(io_write_fd, file_descriptor);
|
||||
ssize_t total_bytes_send = 0;
|
||||
while ((size_t)total_bytes_send < data_size) {
|
||||
size_t chunk = data_size - total_bytes_send;
|
||||
if (io_bwlimit > 0 && chunk > 65536)
|
||||
chunk = 65536;
|
||||
ssize_t bytes_send;
|
||||
if (io_ssl)
|
||||
bytes_send = SSL_write(io_ssl, (const char*)data + total_bytes_send, chunk);
|
||||
else
|
||||
bytes_send = write(fd, (const char*)data + total_bytes_send, chunk);
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
bw_throttle((size_t)bytes_send);
|
||||
total_bytes_send += bytes_send;
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, " Send n Data: %zu", total_bytes_send);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
|
||||
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size);
|
||||
int fd = io_fd(io_read_fd, file_descriptor);
|
||||
size_t total_bytes_received = 0;
|
||||
while (total_bytes_received < data_size) {
|
||||
ssize_t bytes_received;
|
||||
if (io_ssl)
|
||||
bytes_received =
|
||||
SSL_read(io_ssl, (char*)data + total_bytes_received, data_size - total_bytes_received);
|
||||
else
|
||||
bytes_received =
|
||||
read(fd, (char*)data + total_bytes_received, data_size - total_bytes_received);
|
||||
if (bytes_received <= 0) {
|
||||
if (io_ssl) {
|
||||
int ssl_err = SSL_get_error(io_ssl, (int)bytes_received);
|
||||
if (ssl_err == SSL_ERROR_WANT_READ || ssl_err == SSL_ERROR_WANT_WRITE)
|
||||
continue;
|
||||
}
|
||||
if (bytes_received == 0)
|
||||
log_message(LOG_LEVEL_ERROR, "Connection closed while receiving data");
|
||||
else
|
||||
log_message(LOG_LEVEL_ERROR, "Could not receive bytes");
|
||||
return false;
|
||||
}
|
||||
total_bytes_received += bytes_received;
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, " Received n Data: %zu", total_bytes_received);
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* status_to_string(Status status) {
|
||||
switch (status) {
|
||||
case STATUS_OK:
|
||||
return "OK";
|
||||
case STATUS_ERROR:
|
||||
return "ERROR";
|
||||
case STATUS_FINISHED:
|
||||
return "FINISHED";
|
||||
case STATUS_NEXT:
|
||||
return "NEXT";
|
||||
case STATUS_CHUNK:
|
||||
return "CHUNK";
|
||||
case STATUS_CHECK:
|
||||
return "CHECK";
|
||||
case STATUS_DELTA_SIGNATURE:
|
||||
return "DELTA_SIGNATURE";
|
||||
case STATUS_DELTA_DATA:
|
||||
return "DELTA_DATA";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (!send_n_data(file_descriptor, &size, sizeof(size_t)))
|
||||
return false;
|
||||
if (!send_n_data(file_descriptor, data, size))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Send String: %s", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
char* receive_str(int file_descriptor) {
|
||||
size_t size;
|
||||
if (!receive_n_data(file_descriptor, &size, sizeof(size_t)))
|
||||
return NULL;
|
||||
if (size > MAX_STRING_SIZE) {
|
||||
log_message(LOG_LEVEL_ERROR, "receive_str: size %zu exceeds maximum %zu", size,
|
||||
(size_t)MAX_STRING_SIZE);
|
||||
return NULL;
|
||||
}
|
||||
char* data = (char*)malloc(size + 1);
|
||||
if (data == NULL)
|
||||
return NULL;
|
||||
if (!receive_n_data(file_descriptor, data, size)) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
data[size] = '\0';
|
||||
log_message(LOG_LEVEL_DEBUG, "Received String: %s", data);
|
||||
return data;
|
||||
}
|
||||
|
||||
bool send_data(int file_descriptor, const Data* data) {
|
||||
unsigned long long data_size = data->size;
|
||||
if (!send_n_data(file_descriptor, &data_size, sizeof(unsigned long long)))
|
||||
return false;
|
||||
if (!send_n_data(file_descriptor, data->data, data_size))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Send %lld data", data_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
Data* receive_data(int file_descriptor) {
|
||||
unsigned long long size = 0;
|
||||
if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long)))
|
||||
return NULL;
|
||||
void* data = malloc((size_t)size);
|
||||
if (data == NULL)
|
||||
return NULL;
|
||||
if (!receive_n_data(file_descriptor, data, (size_t)size)) {
|
||||
free(data);
|
||||
return NULL;
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, "Received %lld data", size);
|
||||
return data_create(data, (size_t)size);
|
||||
}
|
||||
|
||||
bool send_int(int file_descriptor, int data) {
|
||||
if (!send_n_data(file_descriptor, &data, sizeof(int)))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Send Int: %d", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool receive_int(int file_descriptor, int* data) {
|
||||
if (!receive_n_data(file_descriptor, data, sizeof(int)))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Received Int: %d", *data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool send_status(int file_descriptor, Status status) {
|
||||
if (!send_n_data(file_descriptor, &status, sizeof(Status)))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Send Status: %s", status_to_string(status));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool receive_status(int file_descriptor, Status* status) {
|
||||
if (!receive_n_data(file_descriptor, status, sizeof(Status)))
|
||||
return false;
|
||||
log_message(LOG_LEVEL_DEBUG, "Received Status: %s", status_to_string(*status));
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
|
||||
#include "data.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* Maximum allowed string size for receive_str (10 MB) */
|
||||
#define MAX_STRING_SIZE (10 * 1024 * 1024)
|
||||
|
||||
typedef struct ssl_st SSL;
|
||||
|
||||
typedef int Status;
|
||||
enum NET_STATUS {
|
||||
STATUS_OK,
|
||||
STATUS_ERROR,
|
||||
STATUS_FINISHED,
|
||||
STATUS_NEXT,
|
||||
STATUS_CHUNK,
|
||||
STATUS_MANIFEST,
|
||||
STATUS_CHECK,
|
||||
STATUS_DELTA_SIGNATURE,
|
||||
STATUS_DELTA_DATA
|
||||
};
|
||||
|
||||
void io_set_fds(int read_fd, int write_fd);
|
||||
void io_set_bwlimit(unsigned long long bytes_per_sec);
|
||||
void io_set_ssl(SSL* ssl);
|
||||
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 send_str(int file_descriptor, const char* data);
|
||||
char* receive_str(int file_descriptor);
|
||||
bool send_data(int file_descriptor, const Data* data);
|
||||
Data* receive_data(int file_descriptor);
|
||||
bool send_int(int file_descriptor, int data);
|
||||
bool receive_int(int file_descriptor, int* data);
|
||||
bool send_status(int file_descriptor, Status status);
|
||||
bool receive_status(int file_descriptor, Status* status);
|
||||
|
||||
#endif
|
||||
+22
-24
@@ -9,15 +9,14 @@
|
||||
Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
|
||||
Queue* queue = (Queue*)malloc(sizeof(Queue));
|
||||
if (queue == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for queue structure");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("ERROR: Could not allocate memory for queue structure");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
queue->items = malloc(capacity * sizeof(void*));
|
||||
if (queue->items == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for queue items");
|
||||
free(queue);
|
||||
exit(EXIT_FAILURE);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < capacity; ++i) {
|
||||
@@ -47,29 +46,28 @@ void queue_destroy(Queue *queue) {
|
||||
free(queue);
|
||||
}
|
||||
|
||||
bool queue_is_empty(Queue *queue) {
|
||||
bool queue_is_empty(const Queue* queue) {
|
||||
if (queue == NULL)
|
||||
return true;
|
||||
return queue->size == 0;
|
||||
}
|
||||
|
||||
bool queue_is_full(Queue *queue) {
|
||||
bool queue_is_full(const Queue* queue) {
|
||||
if (queue == NULL)
|
||||
return false;
|
||||
return queue->size == queue->capacity;
|
||||
}
|
||||
|
||||
void queue_double_capacity(Queue *queue) {
|
||||
static bool queue_double_capacity(Queue* queue) {
|
||||
if (queue == NULL)
|
||||
return;
|
||||
return false;
|
||||
unsigned int new_capacity = queue->capacity * 2;
|
||||
if (new_capacity <= 1)
|
||||
new_capacity = 100;
|
||||
void** new_items = malloc(new_capacity * sizeof(void*));
|
||||
if (new_items == NULL) {
|
||||
perror("FATAL ERROR: Could not allocate memory for doubling capacity of "
|
||||
"queue.");
|
||||
exit(EXIT_FAILURE);
|
||||
perror("ERROR: Could not allocate memory for doubling capacity of queue.");
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < queue->size; i++)
|
||||
new_items[i] = queue->items[(i + queue->front) % queue->capacity];
|
||||
@@ -78,29 +76,31 @@ void queue_double_capacity(Queue *queue) {
|
||||
queue->front = 0;
|
||||
queue->rear = queue->size;
|
||||
queue->capacity = new_capacity;
|
||||
return true;
|
||||
}
|
||||
|
||||
void queue_enqueue(Queue *queue, void *item) {
|
||||
if (queue == NULL || item == NULL) {
|
||||
perror("ERROR: Cannot enqueue with a null queue or item.\n");
|
||||
exit(EXIT_FAILURE);
|
||||
bool queue_enqueue(Queue* queue, void* item) {
|
||||
if (queue == NULL || item == NULL)
|
||||
return false;
|
||||
if (queue_is_full(queue)) {
|
||||
if (!queue_double_capacity(queue))
|
||||
return false;
|
||||
}
|
||||
if (queue_is_full(queue))
|
||||
queue_double_capacity(queue);
|
||||
queue->items[queue->rear] = item;
|
||||
queue->rear = (queue->rear + 1) % queue->capacity;
|
||||
queue->size++;
|
||||
return true;
|
||||
}
|
||||
|
||||
void queue_enqueue_multithreaded(Queue *queue, void *item, mtx_t *mutex,
|
||||
cnd_t *condition_not_empty,
|
||||
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full) {
|
||||
mtx_lock(mutex);
|
||||
while (queue_is_full(queue))
|
||||
cnd_wait(condition_not_full, mutex);
|
||||
queue_enqueue(queue, item);
|
||||
bool ok = queue_enqueue(queue, item);
|
||||
cnd_signal(condition_not_empty);
|
||||
mtx_unlock(mutex);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void* queue_dequeue(Queue* queue) {
|
||||
@@ -116,10 +116,8 @@ void *queue_dequeue(Queue *queue) {
|
||||
return item;
|
||||
}
|
||||
|
||||
void *queue_dequeue_multithreaded(Queue *queue, mtx_t *mutex,
|
||||
cnd_t *condition_not_empty,
|
||||
cnd_t *condition_not_full,
|
||||
bool *other_thread_done) {
|
||||
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full, const bool* other_thread_done) {
|
||||
mtx_lock(mutex);
|
||||
while (queue_is_empty(queue) && !*other_thread_done)
|
||||
cnd_wait(condition_not_empty, mutex);
|
||||
|
||||
+6
-10
@@ -15,17 +15,13 @@ typedef struct Queue {
|
||||
|
||||
Queue* queue_create(int capacity, void (*destroyer)(void* item));
|
||||
void queue_destroy(Queue* queue);
|
||||
bool queue_is_empty(Queue *queue);
|
||||
bool queue_is_full(Queue *queue);
|
||||
void queue_double_capacity(Queue *queue);
|
||||
void queue_enqueue(Queue *queue, void *item);
|
||||
void queue_enqueue_multithreaded(Queue *queue, void *item, mtx_t *mutex,
|
||||
cnd_t *condition_not_empty,
|
||||
bool queue_is_empty(const Queue* queue);
|
||||
bool queue_is_full(const Queue* queue);
|
||||
bool queue_enqueue(Queue* queue, void* item);
|
||||
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full);
|
||||
void* queue_dequeue(Queue* queue);
|
||||
void *queue_dequeue_multithreaded(Queue *queue, mtx_t *mutex,
|
||||
cnd_t *condition_not_empty,
|
||||
cnd_t *condition_not_full,
|
||||
bool *other_thread_done);
|
||||
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full, const bool* other_thread_done);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
#include "socket.h"
|
||||
#include "log.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
Server *server_create(int port) {
|
||||
Server *server = (Server *)malloc(sizeof(Server));
|
||||
if (server == NULL) {
|
||||
perror("Could not allocate space for Server");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (file_descriptor < 0) {
|
||||
perror("Could not create Socket!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
server->file_descriptor = file_descriptor;
|
||||
int opt = 1;
|
||||
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt,
|
||||
sizeof(opt))) {
|
||||
perror("Error setting a socket option!");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
server->address.sin_family = AF_INET;
|
||||
server->address.sin_addr.s_addr = INADDR_ANY;
|
||||
server->address.sin_port = htons(port);
|
||||
server->address_length = sizeof(server->address);
|
||||
|
||||
if (bind(server->file_descriptor, (struct sockaddr *)&server->address,
|
||||
server->address_length) < 0) {
|
||||
perror("Could not bind server");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
void server_delete(Server *server) {
|
||||
free(server);
|
||||
server = NULL;
|
||||
};
|
||||
|
||||
void server_listen(Server *server, void (*handler)(int file_descriptor)) {
|
||||
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d",
|
||||
server->address.sin_port);
|
||||
if (listen(server->file_descriptor, 3) < 0) {
|
||||
perror("Could not listen on port!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int file_descriptor =
|
||||
accept(server->file_descriptor, (struct sockaddr *)&server->address,
|
||||
&server->address_length);
|
||||
if (server->file_descriptor < 0) {
|
||||
perror("Could not accept the connection");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
log_message(LOG_LEVEL_INFO, "Received Connection");
|
||||
handler(file_descriptor);
|
||||
close(server->file_descriptor);
|
||||
close(file_descriptor);
|
||||
}
|
||||
|
||||
Client *client_create() {
|
||||
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (file_descriptor < 0) {
|
||||
perror("Could not create Socket!");
|
||||
exit(EXIT_FAILURE);
|
||||
};
|
||||
|
||||
Client *client = (Client *)malloc(sizeof(Client));
|
||||
client->file_descriptor = file_descriptor;
|
||||
client->address.sin_family = AF_INET;
|
||||
client->address_length = sizeof(client->address);
|
||||
return client;
|
||||
}
|
||||
|
||||
void client_connect(Client *client, char *host, int port) {
|
||||
client->address.sin_port = htons(port);
|
||||
|
||||
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) {
|
||||
perror("Could not convert host address!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
if (connect(client->file_descriptor, (struct sockaddr *)&client->address,
|
||||
client->address_length) < 0) {
|
||||
perror("Could not connect to Server!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
void client_disconnect(Client *client) { close(client->file_descriptor); }
|
||||
|
||||
void client_delete(Client *client) {
|
||||
if (client == NULL)
|
||||
return;
|
||||
free(client);
|
||||
}
|
||||
|
||||
void send_n_data(int file_descriptor, void *data, size_t data_size) {
|
||||
log_message(LOG_LEVEL_DEBUG, " Sending n Data: %d", data_size);
|
||||
ssize_t total_bytes_send = 0;
|
||||
while (total_bytes_send < data_size) {
|
||||
printf("Trying: %zu\n", data_size - total_bytes_send);
|
||||
ssize_t bytes_send = send(file_descriptor, (char *)data + total_bytes_send,
|
||||
data_size - total_bytes_send, 0);
|
||||
printf("Bytes send: %zd\n", bytes_send);
|
||||
if (bytes_send <= 0) {
|
||||
perror("Could not send data!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
total_bytes_send += bytes_send;
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, " Send n Data: %zu", total_bytes_send);
|
||||
}
|
||||
|
||||
void receive_n_data(int file_descriptor, void *data, size_t data_size) {
|
||||
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %d", data_size);
|
||||
size_t total_bytes_received = 0;
|
||||
while (total_bytes_received < data_size) {
|
||||
long long bytes_received =
|
||||
recv(file_descriptor, data + total_bytes_received,
|
||||
data_size - total_bytes_received, 0);
|
||||
if (bytes_received == -1 || bytes_received == 0) {
|
||||
perror("Could not receive bytes!");
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
total_bytes_received += bytes_received;
|
||||
}
|
||||
log_message(LOG_LEVEL_DEBUG, " Received n Data: %d", total_bytes_received);
|
||||
}
|
||||
|
||||
void send_str(int file_descriptor, char *data) {
|
||||
size_t size = strlen(data);
|
||||
send_n_data(file_descriptor, &size, sizeof(size_t));
|
||||
send_n_data(file_descriptor, data, size);
|
||||
log_message(LOG_LEVEL_DEBUG, "Send String: %s", data);
|
||||
}
|
||||
|
||||
char *receive_str(int file_descriptor) {
|
||||
size_t size;
|
||||
receive_n_data(file_descriptor, &size, sizeof(size_t));
|
||||
char *data = (char *)malloc(size + 1);
|
||||
receive_n_data(file_descriptor, data, size);
|
||||
data[size] = '\0';
|
||||
log_message(LOG_LEVEL_DEBUG, "Received String: %s", data);
|
||||
return data;
|
||||
}
|
||||
|
||||
void send_data(int file_descriptor, void *data, unsigned long long data_size) {
|
||||
send_n_data(file_descriptor, &data_size, sizeof(unsigned long long));
|
||||
send_n_data(file_descriptor, data, data_size);
|
||||
log_message(LOG_LEVEL_DEBUG, "Send %lld data", data_size);
|
||||
}
|
||||
|
||||
Data *receive_data(int file_descriptor) {
|
||||
size_t size = 0;
|
||||
receive_n_data(file_descriptor, &size, sizeof(unsigned long long));
|
||||
void *data = malloc(size);
|
||||
receive_n_data(file_descriptor, data, size);
|
||||
log_message(LOG_LEVEL_DEBUG, "Received %lld data", size);
|
||||
return data_create(data, size);
|
||||
}
|
||||
|
||||
void send_int(int file_descriptor, int data) {
|
||||
send_n_data(file_descriptor, &data, sizeof(int));
|
||||
log_message(LOG_LEVEL_DEBUG, "Send Int: %d", data);
|
||||
}
|
||||
|
||||
int receive_int(int file_descriptor) {
|
||||
int data;
|
||||
receive_n_data(file_descriptor, &data, sizeof(int));
|
||||
log_message(LOG_LEVEL_DEBUG, "Received Int: %d", data);
|
||||
return data;
|
||||
}
|
||||
|
||||
const char *status_to_string(Status status) {
|
||||
switch (status) {
|
||||
case STATUS_OK:
|
||||
return "OK";
|
||||
case STATUS_ERROR:
|
||||
return "ERROR";
|
||||
case STATUS_FINISHED:
|
||||
return "FINISHED";
|
||||
case STATUS_NEXT:
|
||||
return "NEXT";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void send_status(int file_descriptor, Status status) {
|
||||
send_n_data(file_descriptor, &status, sizeof(Status));
|
||||
log_message(LOG_LEVEL_DEBUG, "Send Status: %s", status_to_string(status));
|
||||
}
|
||||
|
||||
Status receive_status(int file_descriptor) {
|
||||
Status data;
|
||||
receive_n_data(file_descriptor, &data, sizeof(Status));
|
||||
log_message(LOG_LEVEL_DEBUG, "Received Status: %s", status_to_string(data));
|
||||
return data;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#ifndef SOCKET_H
|
||||
#define SOCKET_H
|
||||
|
||||
#include "data.h"
|
||||
#include <netinet/in.h>
|
||||
|
||||
typedef int Status;
|
||||
enum NET_STATUS { STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT };
|
||||
|
||||
typedef struct Server {
|
||||
struct sockaddr_in address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
} Server;
|
||||
|
||||
Server *server_create(int port);
|
||||
void server_listen(Server *server, void (*handler)(int file_descriptor));
|
||||
void server_delete(Server *server);
|
||||
|
||||
typedef struct Client {
|
||||
struct sockaddr_in address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
} Client;
|
||||
|
||||
Client *client_create();
|
||||
void client_disconnect(Client *client);
|
||||
void client_delete(Client *client);
|
||||
void client_connect(Client *client, char *host, int port);
|
||||
|
||||
void send_n_data(int file_descriptor, void *data, size_t data_size);
|
||||
void receive_n_data(int file_descriptor, void *data, size_t data_size);
|
||||
void send_str(int file_descriptor, char *data);
|
||||
char *receive_str(int file_descriptor);
|
||||
void send_data(int file_descriptor, void *data, unsigned long long data_size);
|
||||
Data *receive_data(int file_descriptor);
|
||||
void send_int(int file_descriptor, int data);
|
||||
int receive_int(int file_descriptor);
|
||||
void send_status(int file_descriptor, Status status);
|
||||
Status receive_status(int file_descriptor);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "transport_ssh.h"
|
||||
#include "utils.h"
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
char* user;
|
||||
char* host;
|
||||
char* remote_path;
|
||||
} RemoteDest;
|
||||
|
||||
static void remote_dest_destroy(RemoteDest* r) {
|
||||
free(r->user);
|
||||
free(r->host);
|
||||
free(r->remote_path);
|
||||
}
|
||||
|
||||
static int parse_remote_dest(const char* dest, RemoteDest* r) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
const char* colon = strchr(dest, ':');
|
||||
if (!colon)
|
||||
return -1;
|
||||
|
||||
r->remote_path = str_dup(colon + 1);
|
||||
if (!r->remote_path)
|
||||
return -1;
|
||||
|
||||
const char* at = memchr(dest, '@', colon - dest);
|
||||
if (at) {
|
||||
size_t user_len = at - dest;
|
||||
r->user = malloc(user_len + 1);
|
||||
if (!r->user) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
memcpy(r->user, dest, user_len);
|
||||
r->user[user_len] = '\0';
|
||||
|
||||
size_t host_len = colon - at - 1;
|
||||
r->host = malloc(host_len + 1);
|
||||
if (!r->host) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
memcpy(r->host, at + 1, host_len);
|
||||
r->host[host_len] = '\0';
|
||||
} else {
|
||||
r->user = str_dup("");
|
||||
if (!r->user) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
size_t host_len = colon - dest;
|
||||
r->host = malloc(host_len + 1);
|
||||
if (!r->host) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
memcpy(r->host, dest, host_len);
|
||||
r->host[host_len] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Client* client_connect_ssh(const char* destination, int port) {
|
||||
RemoteDest r;
|
||||
if (parse_remote_dest(destination, &r) != 0) {
|
||||
fprintf(stderr, "Invalid remote destination: %s\n", destination);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
|
||||
perror("socketpair failed");
|
||||
remote_dest_destroy(&r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int buf_size = 1024 * 1024;
|
||||
setsockopt(sv[0], SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(sv[0], SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(sv[1], SOL_SOCKET, SO_SNDBUF, &buf_size, sizeof(buf_size));
|
||||
setsockopt(sv[1], SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(buf_size));
|
||||
|
||||
int exec_pipe[2];
|
||||
if (pipe(exec_pipe) < 0) {
|
||||
perror("pipe failed");
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
remote_dest_destroy(&r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
perror("fork failed");
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
close(exec_pipe[0]);
|
||||
close(exec_pipe[1]);
|
||||
remote_dest_destroy(&r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
close(sv[0]);
|
||||
close(exec_pipe[0]);
|
||||
fcntl(exec_pipe[1], F_SETFD, FD_CLOEXEC);
|
||||
if (sv[1] != STDIN_FILENO)
|
||||
dup2(sv[1], STDIN_FILENO);
|
||||
if (sv[1] != STDOUT_FILENO)
|
||||
dup2(sv[1], STDOUT_FILENO);
|
||||
if (sv[1] > 1)
|
||||
close(sv[1]);
|
||||
|
||||
char ssh_user[512];
|
||||
if (r.user && r.user[0] != '\0')
|
||||
snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
|
||||
else
|
||||
snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
|
||||
|
||||
size_t ssh_argv_max = 32;
|
||||
char** ssh_argv = calloc(ssh_argv_max, sizeof(char*));
|
||||
if (ssh_argv == NULL)
|
||||
_exit(1);
|
||||
int ac = 0;
|
||||
char port_str[16];
|
||||
ssh_argv[ac++] = "ssh";
|
||||
ssh_argv[ac++] = "-o";
|
||||
ssh_argv[ac++] = "Compression=no";
|
||||
ssh_argv[ac++] = "-o";
|
||||
ssh_argv[ac++] = "ControlMaster=auto";
|
||||
ssh_argv[ac++] = "-o";
|
||||
ssh_argv[ac++] = "ControlPath=~/.cache/fastsync-%r@%h:%p";
|
||||
if (port > 0 && port != 22) {
|
||||
if ((size_t)ac + 2 >= ssh_argv_max) {
|
||||
free(ssh_argv);
|
||||
_exit(1);
|
||||
}
|
||||
ssh_argv[ac++] = "-p";
|
||||
snprintf(port_str, sizeof(port_str), "%d", port);
|
||||
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++] = "fastsync-server";
|
||||
ssh_argv[ac++] = "--stdio";
|
||||
ssh_argv[ac] = NULL;
|
||||
execvp("ssh", ssh_argv);
|
||||
free(ssh_argv);
|
||||
perror("exec of ssh failed");
|
||||
ssize_t wret = write(exec_pipe[1], "x", 1);
|
||||
(void)wret;
|
||||
_exit(1);
|
||||
}
|
||||
|
||||
close(sv[1]);
|
||||
close(exec_pipe[1]);
|
||||
|
||||
char exec_status;
|
||||
ssize_t n = read(exec_pipe[0], &exec_status, 1);
|
||||
close(exec_pipe[0]);
|
||||
|
||||
if (n > 0) {
|
||||
close(sv[0]);
|
||||
waitpid(pid, NULL, 0);
|
||||
remote_dest_destroy(&r);
|
||||
fprintf(stderr, "Error: could not launch 'fastsync-server --stdio' on remote\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
remote_dest_destroy(&r);
|
||||
|
||||
Client* client = malloc(sizeof(Client));
|
||||
if (client == NULL) {
|
||||
close(sv[0]);
|
||||
waitpid(pid, NULL, 0);
|
||||
return NULL;
|
||||
}
|
||||
client->file_descriptor = sv[0];
|
||||
client->address.ss_family = AF_UNIX;
|
||||
client->address_length = 0;
|
||||
client->ssh_child_pid = pid;
|
||||
client->ssl = NULL;
|
||||
client->ssl_ctx = NULL;
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#ifndef TRANSPORT_SSH_H
|
||||
#define TRANSPORT_SSH_H
|
||||
|
||||
#include "transport_tcp.h"
|
||||
|
||||
Client* client_connect_ssh(const char* destination, int port);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,293 @@
|
||||
#include "transport_tcp.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
bool set_socket_timeouts(int fd) {
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 30;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int keepalive = 1;
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive)) < 0) {
|
||||
perror("Could not set SO_KEEPALIVE");
|
||||
return false;
|
||||
}
|
||||
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 = (Server*)malloc(sizeof(Server));
|
||||
if (server == NULL) {
|
||||
perror("Could not allocate space for Server");
|
||||
return NULL;
|
||||
}
|
||||
memset(&server->address, 0, sizeof(server->address));
|
||||
|
||||
// Try IPv6 first, fall back to IPv4
|
||||
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!");
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!set_socket_timeouts(fd)) {
|
||||
close(fd);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
server->file_descriptor = fd;
|
||||
int opt = 1;
|
||||
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
|
||||
perror("Error setting a socket option!");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Determine address family from the actual socket
|
||||
struct sockaddr_storage* addr = &server->address;
|
||||
socklen_t addr_len = sizeof(*addr);
|
||||
if (getsockname(fd, (struct sockaddr*)addr, &addr_len) == 0) {
|
||||
// Use the family of the socket we actually created
|
||||
}
|
||||
|
||||
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) <
|
||||
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");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
perror("Could not bind server");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
void server_delete(Server** server) {
|
||||
if (server == NULL || *server == NULL)
|
||||
return;
|
||||
close((*server)->file_descriptor);
|
||||
if ((*server)->ssl_ctx) {
|
||||
SSL_CTX_free((*server)->ssl_ctx);
|
||||
(*server)->ssl_ctx = NULL;
|
||||
}
|
||||
free(*server);
|
||||
*server = NULL;
|
||||
}
|
||||
|
||||
static void accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
||||
const char* log_fmt) {
|
||||
if (listen(server->file_descriptor, SOMAXCONN) < 0) {
|
||||
perror("Could not listen on port!");
|
||||
return;
|
||||
}
|
||||
signal(SIGCHLD, SIG_IGN);
|
||||
while (1) {
|
||||
struct sockaddr_storage client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
|
||||
if (fd < 0) {
|
||||
perror("Could not accept the connection");
|
||||
continue;
|
||||
}
|
||||
set_socket_timeouts(fd);
|
||||
log_message(LOG_LEVEL_INFO, "%s", log_fmt);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(server->file_descriptor);
|
||||
child_fn(fd, child_ctx);
|
||||
close(fd);
|
||||
_exit(0);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
struct plain_ctx {
|
||||
void (*handler)(int);
|
||||
};
|
||||
|
||||
static void plain_child_fn(int fd, void* ctx) {
|
||||
((struct plain_ctx*)ctx)->handler(fd);
|
||||
}
|
||||
|
||||
bool server_listen(Server* server, void (*handler)(int file_descriptor)) {
|
||||
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};
|
||||
accept_loop(server, plain_child_fn, &ctx, "Received Connection");
|
||||
return true;
|
||||
}
|
||||
|
||||
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
||||
const char* log_fmt) {
|
||||
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);
|
||||
}
|
||||
|
||||
Client* client_create() {
|
||||
Client* client = (Client*)malloc(sizeof(Client));
|
||||
if (client == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
memset(&client->address, 0, sizeof(client->address));
|
||||
client->address.ss_family = AF_UNSPEC;
|
||||
client->address_length = sizeof(client->address);
|
||||
client->file_descriptor = -1;
|
||||
client->ssh_child_pid = -1;
|
||||
client->ssl = NULL;
|
||||
client->ssl_ctx = NULL;
|
||||
return client;
|
||||
}
|
||||
|
||||
bool client_connect(Client* client, char* host, int port) {
|
||||
struct addrinfo hints, *res, *rp;
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
|
||||
char port_str[16];
|
||||
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;
|
||||
}
|
||||
|
||||
// Try IPv6 first, then IPv4
|
||||
int fd = -1;
|
||||
for (rp = res; rp != NULL; rp = rp->ai_next) {
|
||||
fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
|
||||
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 (fd < 0) {
|
||||
perror("Could not connect to Server!");
|
||||
freeaddrinfo(res);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
void client_disconnect(Client* client) {
|
||||
if (client->ssl) {
|
||||
SSL_shutdown(client->ssl);
|
||||
SSL_free(client->ssl);
|
||||
client->ssl = NULL;
|
||||
io_set_ssl(NULL);
|
||||
}
|
||||
if (client->file_descriptor >= 0) {
|
||||
close(client->file_descriptor);
|
||||
client->file_descriptor = -1;
|
||||
}
|
||||
if (client->ssh_child_pid > 0) {
|
||||
int status;
|
||||
waitpid(client->ssh_child_pid, &status, 0);
|
||||
client->ssh_child_pid = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void client_delete(Client* client) {
|
||||
if (client == NULL)
|
||||
return;
|
||||
if (client->ssl_ctx) {
|
||||
SSL_CTX_free(client->ssl_ctx);
|
||||
client->ssl_ctx = NULL;
|
||||
}
|
||||
free(client);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef TRANSPORT_TCP_H
|
||||
#define TRANSPORT_TCP_H
|
||||
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
typedef struct Server {
|
||||
struct sockaddr_storage address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
void* ssl_ctx;
|
||||
} Server;
|
||||
|
||||
typedef struct Client {
|
||||
struct sockaddr_storage address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
pid_t ssh_child_pid;
|
||||
void* ssl;
|
||||
void* ssl_ctx;
|
||||
} Client;
|
||||
|
||||
Server* server_create(int port);
|
||||
bool server_listen(Server* server, void (*handler)(int file_descriptor));
|
||||
void server_accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
||||
const char* log_fmt);
|
||||
void server_delete(Server** server);
|
||||
Client* client_create();
|
||||
bool client_connect(Client* client, char* host, int port);
|
||||
void client_disconnect(Client* client);
|
||||
void client_delete(Client* client);
|
||||
bool set_socket_timeouts(int fd);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "transport_tls.h"
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include "transport_tcp.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
bool tls_global_init(void) {
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
SSL_library_init();
|
||||
OpenSSL_add_all_algorithms();
|
||||
SSL_load_error_strings();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
static void log_ssl_errors(void) {
|
||||
unsigned long err;
|
||||
char buf[256];
|
||||
while ((err = ERR_get_error()) != 0) {
|
||||
ERR_error_string_n(err, buf, sizeof(buf));
|
||||
log_message(LOG_LEVEL_ERROR, "SSL error: %s", buf);
|
||||
}
|
||||
}
|
||||
|
||||
static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key,
|
||||
const char* ca_path) {
|
||||
const SSL_METHOD* method = is_server ? TLS_server_method() : TLS_client_method();
|
||||
SSL_CTX* ctx = SSL_CTX_new(method);
|
||||
if (!ctx) {
|
||||
log_message(LOG_LEVEL_ERROR, "Unable to create SSL context");
|
||||
log_ssl_errors();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
|
||||
|
||||
if (cert && key) {
|
||||
if (SSL_CTX_use_certificate_file(ctx, cert, SSL_FILETYPE_PEM) <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to load certificate: %s", cert);
|
||||
log_ssl_errors();
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
if (SSL_CTX_use_PrivateKey_file(ctx, key, SSL_FILETYPE_PEM) <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to load private key: %s", key);
|
||||
log_ssl_errors();
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
if (!SSL_CTX_check_private_key(ctx)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Private key does not match certificate");
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (ca_path) {
|
||||
if (!SSL_CTX_load_verify_locations(ctx, ca_path, NULL)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to load CA: %s", ca_path);
|
||||
log_ssl_errors();
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
|
||||
SSL_CTX_set_verify_depth(ctx, 4);
|
||||
} 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);
|
||||
}
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server) {
|
||||
SSL* ssl = SSL_new(ctx);
|
||||
if (!ssl) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to create SSL object");
|
||||
return NULL;
|
||||
}
|
||||
SSL_set_fd(ssl, fd);
|
||||
|
||||
int ret;
|
||||
if (is_server)
|
||||
ret = SSL_accept(ssl);
|
||||
else
|
||||
ret = SSL_connect(ssl);
|
||||
|
||||
if (ret <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "SSL %s failed", is_server ? "accept" : "connect");
|
||||
log_ssl_errors();
|
||||
SSL_free(ssl);
|
||||
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;
|
||||
}
|
||||
|
||||
bool server_create_tls(Server* server, const char* cert_path, const char* key_path,
|
||||
const char* ca_path) {
|
||||
SSL_CTX* ctx = create_ssl_ctx(true, cert_path, key_path, ca_path);
|
||||
if (!ctx)
|
||||
return false;
|
||||
server->ssl_ctx = ctx;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct tls_child_ctx {
|
||||
void (*handler)(int);
|
||||
SSL_CTX* ssl_ctx;
|
||||
};
|
||||
|
||||
static void tls_child_fn(int fd, void* arg) {
|
||||
struct tls_child_ctx* ctx = (struct tls_child_ctx*)arg;
|
||||
SSL* ssl = wrap_fd_with_ssl(fd, ctx->ssl_ctx, true);
|
||||
if (!ssl)
|
||||
return;
|
||||
io_set_ssl(ssl);
|
||||
ctx->handler(fd);
|
||||
SSL_shutdown(ssl);
|
||||
SSL_free(ssl);
|
||||
io_set_ssl(NULL);
|
||||
}
|
||||
|
||||
bool server_listen_tls(Server* server, void (*handler)(int file_descriptor)) {
|
||||
struct tls_child_ctx ctx = {handler, (SSL_CTX*)server->ssl_ctx};
|
||||
server_accept_loop(server, tls_child_fn, &ctx, "Received TLS Connection");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool client_connect_tls(Client* client, char* host, int port, const char* cert_path,
|
||||
const char* key_path, const char* ca_path) {
|
||||
// Use the common TCP connection logic (with IPv6 support)
|
||||
if (!client_connect(client, host, port))
|
||||
return false;
|
||||
|
||||
SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path);
|
||||
if (!ctx)
|
||||
return false;
|
||||
client->ssl_ctx = ctx;
|
||||
|
||||
SSL* ssl = wrap_fd_with_ssl(client->file_descriptor, ctx, false);
|
||||
if (!ssl) {
|
||||
SSL_CTX_free(ctx);
|
||||
client->ssl_ctx = NULL;
|
||||
return false;
|
||||
}
|
||||
client->ssl = ssl;
|
||||
io_set_ssl(ssl);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef TRANSPORT_TLS_H
|
||||
#define TRANSPORT_TLS_H
|
||||
|
||||
#include "transport_tcp.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
bool tls_global_init(void);
|
||||
|
||||
bool server_create_tls(Server* server, const char* cert_path, const char* key_path,
|
||||
const char* ca_path);
|
||||
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,
|
||||
const char* key_path, const char* ca_path);
|
||||
|
||||
#endif
|
||||
+157
-30
@@ -1,42 +1,65 @@
|
||||
#include "utils.h"
|
||||
#include "array_list.h"
|
||||
#include "libgen.h"
|
||||
#include "sys/stat.h"
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
void mkdir_r(char *path) {
|
||||
char *path_duplicate = malloc(strlen(path) + 1);
|
||||
strcpy(path_duplicate, path);
|
||||
char *path_current = (char *)malloc((strlen(path) + 2) * sizeof(char));
|
||||
char *path_current_position = path_current;
|
||||
bool mkdir_r(const char* path) {
|
||||
size_t path_len = strlen(path);
|
||||
char* path_duplicate = malloc(path_len + 1);
|
||||
if (!path_duplicate)
|
||||
return false;
|
||||
memcpy(path_duplicate, path, path_len + 1);
|
||||
/* 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) {
|
||||
free(path_duplicate);
|
||||
return false;
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (path[0] == '/') {
|
||||
strcpy(path_current, "/");
|
||||
path_current_position += 1;
|
||||
path_current[0] = '/';
|
||||
path_current[1] = '\0';
|
||||
pos = 1;
|
||||
} else {
|
||||
path_current[0] = '\0';
|
||||
}
|
||||
const char* delimiter = "/";
|
||||
char *part = strtok(path_duplicate, delimiter);
|
||||
char* saveptr;
|
||||
const char* part = strtok_r(path_duplicate, delimiter, &saveptr);
|
||||
bool ok = true;
|
||||
while (part != NULL) {
|
||||
strcpy(path_current_position, part);
|
||||
path_current_position += strlen(part) * sizeof(char);
|
||||
strcpy(path_current_position, "/");
|
||||
path_current_position += sizeof(char);
|
||||
size_t part_len = strlen(part);
|
||||
if (pos + part_len + 1 >= buf_size) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
memcpy(path_current + pos, part, part_len);
|
||||
pos += part_len;
|
||||
path_current[pos] = '/';
|
||||
pos++;
|
||||
path_current[pos] = '\0';
|
||||
struct stat st;
|
||||
if (stat(path_current, &st) != 0) {
|
||||
if (mkdir(path_current, 0755) != 0) {
|
||||
perror("Could not create directory");
|
||||
exit(EXIT_FAILURE);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
part = strtok(NULL, delimiter);
|
||||
part = strtok_r(NULL, delimiter, &saveptr);
|
||||
}
|
||||
free(path_duplicate);
|
||||
free(path_current);
|
||||
return ok;
|
||||
}
|
||||
|
||||
char *str_dup(char *string) {
|
||||
char* str_dup(const char* string) {
|
||||
if (string == NULL)
|
||||
return NULL;
|
||||
char* new_string = (char*)malloc(strlen(string) + 1);
|
||||
@@ -44,22 +67,124 @@ char *str_dup(char *string) {
|
||||
return new_string;
|
||||
}
|
||||
|
||||
void to_disk(char *path, void *data, unsigned long long data_size) {
|
||||
char *directory = str_dup(path);
|
||||
char *dir_to_free = directory;
|
||||
directory = dirname(directory);
|
||||
mkdir_r(directory);
|
||||
FILE *file_pointer = fopen(path, "wb");
|
||||
if (file_pointer == NULL) {
|
||||
perror("Could not open File");
|
||||
exit(EXIT_FAILURE);
|
||||
bool glob_match(const char* pattern, const char* str) {
|
||||
while (*pattern) {
|
||||
if (*pattern == '*') {
|
||||
/* Check for double-star (globstar) pattern */
|
||||
if (*(pattern + 1) == '*') {
|
||||
pattern += 2;
|
||||
/* Trailing double-star matches everything */
|
||||
if (*pattern == '\0')
|
||||
return true;
|
||||
/* double-star slash: match at any depth */
|
||||
if (*pattern == '/')
|
||||
pattern++;
|
||||
while (*str) {
|
||||
if (glob_match(pattern, str))
|
||||
return true;
|
||||
str++;
|
||||
}
|
||||
fwrite(data, 1, data_size, file_pointer);
|
||||
fclose(file_pointer);
|
||||
free(dir_to_free);
|
||||
return glob_match(pattern, str);
|
||||
}
|
||||
/* Single * — does not cross / boundaries */
|
||||
pattern++;
|
||||
while (*str && *str != '/') {
|
||||
if (glob_match(pattern, str))
|
||||
return true;
|
||||
str++;
|
||||
}
|
||||
return glob_match(pattern, str);
|
||||
} else if (*pattern == '?') {
|
||||
if (!*str || *str == '/')
|
||||
return false;
|
||||
pattern++;
|
||||
str++;
|
||||
} else {
|
||||
if (*pattern != *str) {
|
||||
/* If pattern has a '/' followed by '**', allow zero path components */
|
||||
if (*pattern == '/' && *(pattern + 1) == '*' && *(pattern + 2) == '*') {
|
||||
/* Skip over slash-double-star and try to match rest against current str */
|
||||
const char* rest = pattern + 3;
|
||||
if (*rest == '/')
|
||||
rest++;
|
||||
return glob_match(rest, str);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
pattern++;
|
||||
str++;
|
||||
}
|
||||
}
|
||||
return *str == '\0';
|
||||
}
|
||||
|
||||
char *path_cat(char *path1, char *path2) {
|
||||
static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
|
||||
size_t len = strlen(rel_path);
|
||||
for (int i = 0; i < manifest->size; i++) {
|
||||
const char* entry = (const char*)manifest->items[i];
|
||||
// Check if entry starts with rel_path + '/' or matches exactly
|
||||
if (strncmp(entry, rel_path, len) == 0 && (entry[len] == '/' || entry[len] == '\0'))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void delete_extras_walk(const char* abs_path, const char* rel_path, ArrayList* manifest) {
|
||||
DIR* dir = opendir(abs_path);
|
||||
if (!dir)
|
||||
return;
|
||||
bool all_removed = true;
|
||||
struct dirent* entry;
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
char* child_abs = path_cat((char*)abs_path, entry->d_name);
|
||||
char* child_rel = path_cat((char*)rel_path, entry->d_name);
|
||||
struct stat st;
|
||||
if (stat(child_abs, &st) != 0) {
|
||||
free(child_abs);
|
||||
free(child_rel);
|
||||
continue;
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
delete_extras_walk(child_abs, child_rel, manifest);
|
||||
// After recursion, try to remove the subdirectory if it's now empty.
|
||||
// Ignore ENOENT: the recursive call may have already removed it.
|
||||
if (rmdir(child_abs) != 0 && errno != ENOENT) {
|
||||
all_removed = false;
|
||||
}
|
||||
} else {
|
||||
// Check if relative path is in manifest
|
||||
bool found = false;
|
||||
for (int i = 0; i < manifest->size; i++) {
|
||||
if (strcmp((char*)manifest->items[i], child_rel) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
unlink(child_abs);
|
||||
fprintf(stderr, " Deleted: %s\n", child_rel);
|
||||
} else {
|
||||
all_removed = false;
|
||||
}
|
||||
}
|
||||
free(child_abs);
|
||||
free(child_rel);
|
||||
}
|
||||
closedir(dir);
|
||||
// Only remove the directory itself if it is not in the manifest
|
||||
// and contained no kept entries.
|
||||
if (all_removed && rel_path[0] != '\0' && !is_dir_in_manifest(rel_path, manifest)) {
|
||||
rmdir(abs_path);
|
||||
}
|
||||
}
|
||||
|
||||
void delete_extras(const char* dest_root, ArrayList* manifest) {
|
||||
delete_extras_walk(dest_root, "", manifest);
|
||||
}
|
||||
|
||||
char* path_cat(const char* path1, char* path2) {
|
||||
if (path1 == NULL || *path1 == '\0')
|
||||
return str_dup(path2);
|
||||
if (path2 == NULL || *path2 == '\0')
|
||||
@@ -74,6 +199,8 @@ char *path_cat(char *path1, char *path2) {
|
||||
path2_len -= 1;
|
||||
}
|
||||
char* new_path = malloc(path1_len + path2_len + 2);
|
||||
if (new_path == NULL)
|
||||
return NULL;
|
||||
memcpy(new_path, path1, path1_len);
|
||||
new_path[path1_len] = '/';
|
||||
memcpy(new_path + path1_len + 1, path2_pointer, path2_len);
|
||||
|
||||
+8
-4
@@ -1,9 +1,13 @@
|
||||
#ifndef UTILS_H
|
||||
#define UTILS_H
|
||||
|
||||
void mkdir_r(char *path);
|
||||
char *str_dup(char *string);
|
||||
void to_disk(char *path, void *data, unsigned long long data_size);
|
||||
char *path_cat(char *path1, char *path2);
|
||||
#include "array_list.h"
|
||||
#include <stdbool.h>
|
||||
|
||||
bool mkdir_r(const char* path);
|
||||
char* str_dup(const char* string);
|
||||
char* path_cat(const char* path1, char* path2);
|
||||
bool glob_match(const char* pattern, const char* str);
|
||||
void delete_extras(const char* dest_root, ArrayList* manifest);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# --- Configuration ---
|
||||
SERVER_CMD = ["./build/server"]
|
||||
base_client_cmd = ["./build/client"]
|
||||
|
||||
# --- Resource Limit Configuration ---
|
||||
# 💾 Disk throttling settings
|
||||
DISK_DEVICE = (
|
||||
"/dev/nvme0n1p5" # IMPORTANT: Change this to your disk (e.g., /dev/nvme0n1)
|
||||
)
|
||||
READ_BPS_MAX = "15M" # Max read speed (M for megabytes)
|
||||
WRITE_BPS_MAX = "10M" # Max write speed
|
||||
|
||||
# 🐢 Network throttling settings (Linux tc)
|
||||
NET_LIMIT = "100mbit"
|
||||
NET_DELAY = "100ms"
|
||||
NETWORK_INTERFACE = "lo"
|
||||
NET_LIMIT_CMD = f"sudo tc qdisc add dev {NETWORK_INTERFACE} root netem rate {NET_LIMIT} delay {NET_DELAY}".split()
|
||||
NET_RESET_CMD = f"sudo tc qdisc del dev {NETWORK_INTERFACE} root".split()
|
||||
|
||||
# --- Build the client command prefix with throttling ---
|
||||
CLIENT_CMD_PREFIX = [
|
||||
"sudo",
|
||||
"systemd-run",
|
||||
"--scope",
|
||||
"-p",
|
||||
f"IOReadBandwidthMax={DISK_DEVICE} {READ_BPS_MAX}",
|
||||
"-p",
|
||||
f"IOWriteBandwidthMax={DISK_DEVICE} {WRITE_BPS_MAX}",
|
||||
]
|
||||
|
||||
# --- Test Cases ---
|
||||
TEST_CASES = [
|
||||
{"name": "Standard (Single-threaded)", "flags": []},
|
||||
{"name": "Multithreading (-m)", "flags": ["-m"]},
|
||||
{"name": "Compression (-c -5)", "flags": ["-c -5"]},
|
||||
{"name": "Compression (-c 0)", "flags": ["-c 0"]},
|
||||
{"name": "Compression (-c 10)", "flags": ["-c 10"]},
|
||||
{"name": "Compression (-c 20)", "flags": ["-c 20"]},
|
||||
# {"name": "Chunk Serialization (-s)", "flags": ["-s"]},
|
||||
{"name": "Multithreading + Compression (-m -c)", "flags": ["-m", "-c"]},
|
||||
# {"name": "Multithreading + Chunk Serialization (-m -s)", "flags": ["-m", "-s"]},
|
||||
# {"name": "Compression + Chunk Serialization (-c -s)", "flags": ["-c", "-s"]},
|
||||
# {
|
||||
# "name": "Multithreading + Compression + Chunk Serialization (-m -c -s)",
|
||||
# "flags": ["-m", "-c", "-s"],
|
||||
# },
|
||||
]
|
||||
|
||||
|
||||
def run_suite(env_name, apply_limits):
|
||||
results = []
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"🚀 Starting Suite: {env_name}")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
if apply_limits:
|
||||
print(
|
||||
f"Applying Disk I/O Limits: Reads <= {READ_BPS_MAX}, Writes <= {WRITE_BPS_MAX}"
|
||||
)
|
||||
print(f"Applying Network Limits: {NET_LIMIT}, {NET_DELAY} delay")
|
||||
client_prefix = CLIENT_CMD_PREFIX
|
||||
else:
|
||||
print("Running Baseline (No limits applied)")
|
||||
client_prefix = [] # Run normally without systemd-run/limits
|
||||
|
||||
try:
|
||||
# SETUP: Apply or ensure clean network limits
|
||||
if apply_limits:
|
||||
subprocess.run(NET_LIMIT_CMD, check=True)
|
||||
else:
|
||||
# Silently attempt to clear any leftover rules just to ensure a clean baseline
|
||||
subprocess.run(NET_RESET_CMD, capture_output=True)
|
||||
|
||||
for case in TEST_CASES:
|
||||
name = case["name"]
|
||||
flags = case["flags"]
|
||||
|
||||
print(f"\n--- Running: {name} ---")
|
||||
|
||||
server_process = None
|
||||
try:
|
||||
# 1. Start the server
|
||||
print(" Starting server...")
|
||||
server_process = subprocess.Popen(
|
||||
SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None
|
||||
)
|
||||
time.sleep(0.5) # Allow server to bind to port
|
||||
|
||||
# 2. Build and run the client
|
||||
client_cmd = client_prefix + base_client_cmd + flags
|
||||
print(f" Running client: {' '.join(client_cmd)}")
|
||||
|
||||
start_time = time.monotonic()
|
||||
client_result = subprocess.run(
|
||||
client_cmd, text=True, capture_output=True
|
||||
)
|
||||
end_time = time.monotonic()
|
||||
|
||||
duration = end_time - start_time
|
||||
|
||||
if client_result.returncode == 0:
|
||||
results.append(
|
||||
{
|
||||
"environment": env_name,
|
||||
"name": name,
|
||||
"status": "Success",
|
||||
"time": f"{duration:.4f}s",
|
||||
"error": "",
|
||||
}
|
||||
)
|
||||
else:
|
||||
print(f" ⚠️ Failed (code: {client_result.returncode})")
|
||||
err_msg = (
|
||||
client_result.stderr.strip().split("\n")[0]
|
||||
if client_result.stderr
|
||||
else (
|
||||
client_result.stdout.strip().split("\n")[0]
|
||||
if client_result.stdout
|
||||
else "No output"
|
||||
)
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"environment": env_name,
|
||||
"name": name,
|
||||
"status": "Failed",
|
||||
"time": "N/A",
|
||||
"error": f"Exit code {client_result.returncode}: {err_msg[:40]}",
|
||||
}
|
||||
)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(" ⚠️ Timeout (exceeded 15s)")
|
||||
results.append(
|
||||
{
|
||||
"environment": env_name,
|
||||
"name": name,
|
||||
"status": "Timeout",
|
||||
"time": "N/A",
|
||||
"error": "Exceeded 15 seconds",
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
results.append(
|
||||
{
|
||||
"environment": env_name,
|
||||
"name": name,
|
||||
"status": "Error",
|
||||
"time": "N/A",
|
||||
"error": str(e),
|
||||
}
|
||||
)
|
||||
finally:
|
||||
# Clean up the server for this test case
|
||||
if server_process:
|
||||
print(" Stopping server...")
|
||||
try:
|
||||
server_process.terminate()
|
||||
server_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
server_process.kill()
|
||||
server_process.wait()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"❌ Error running system limit command: {' '.join(e.cmd)}")
|
||||
print("Are you running this script with 'sudo' privileges?")
|
||||
|
||||
finally:
|
||||
# TEARDOWN: Remove network limits if they were applied
|
||||
if apply_limits:
|
||||
print("\nCleaning up limits for this suite...")
|
||||
try:
|
||||
subprocess.run(NET_RESET_CMD, check=True, capture_output=True)
|
||||
print("Network limits removed.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not reset network settings: {e}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
os.system("cmake -B build -S .")
|
||||
os.system("cd build && make")
|
||||
|
||||
# --- Main Execution ---
|
||||
all_results = []
|
||||
|
||||
# 1. Run Baseline (No Limits)
|
||||
all_results.extend(run_suite("Unlimited", apply_limits=False))
|
||||
|
||||
# # 2. Run Throttled (With Limits)
|
||||
all_results.extend(run_suite("Throttled", apply_limits=True))
|
||||
|
||||
# --- Print Comparison Table ---
|
||||
print("\n" + "=" * 105)
|
||||
print(f"{'fastSync BENCHMARK RESULTS (COMPARISON)':^105}")
|
||||
print("=" * 105)
|
||||
print(
|
||||
f"{'Configuration':<45} | {'Environment':<12} | {'Status':<10} | {'Time':<10} | {'Details/Error':<20}"
|
||||
)
|
||||
print("-" * 105)
|
||||
|
||||
# Sort results by test case name first, then environment to easily compare
|
||||
# This groups the baseline and throttled results for the same test next to each other
|
||||
# sorted_results = sorted(
|
||||
# all_results,
|
||||
# key=lambda x: (
|
||||
# TEST_CASES.index(
|
||||
# next(item for item in TEST_CASES if item["name"] == x["name"])
|
||||
# ),
|
||||
# x["environment"],
|
||||
# ),
|
||||
# )
|
||||
|
||||
for res in all_results:
|
||||
status_symbol = (
|
||||
"✅"
|
||||
if res["status"] == "Success"
|
||||
else ("⏳" if res["status"] == "Timeout" else "❌")
|
||||
)
|
||||
status_str = f"{status_symbol} {res['status']}"
|
||||
print(
|
||||
f"{res['name']:<45} | {res['environment']:<12} | {status_str:<10} | {res['time']:<10} | {res['error']:<20}"
|
||||
)
|
||||
print("=" * 105)
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Shared pytest configuration for integration tests."""
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "integration"))
|
||||
|
||||
from common import ServerManager
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def shared_server():
|
||||
"""One server for the entire test session. Avoids 27+ server start/stop cycles."""
|
||||
server = ServerManager()
|
||||
server.start()
|
||||
yield server
|
||||
server.stop()
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "chunk.h"
|
||||
#include "data.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
void* buf = malloc(size);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
Data* d = data_create(buf, size);
|
||||
if (!d)
|
||||
return 0;
|
||||
|
||||
Chunk* chunk = chunk_deserialize(d, false);
|
||||
if (chunk)
|
||||
chunk_destroy(chunk);
|
||||
|
||||
data_destroy(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "compression.h"
|
||||
#include "data.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
void* buf = malloc(size);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
Data* d = data_create(buf, size);
|
||||
if (!d)
|
||||
return 0;
|
||||
|
||||
Data* compressed = data_compress(d, 3);
|
||||
if (compressed) {
|
||||
Data* decompressed = data_decompress(compressed);
|
||||
if (decompressed) {
|
||||
data_destroy(decompressed);
|
||||
}
|
||||
data_destroy(compressed);
|
||||
}
|
||||
|
||||
data_destroy(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "delta.h"
|
||||
#include "data.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
void* buf = malloc(size);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
Data* d = data_create(buf, size);
|
||||
if (!d)
|
||||
return 0;
|
||||
|
||||
Delta* delta = delta_deserialize(d);
|
||||
if (delta)
|
||||
delta_destroy(delta);
|
||||
|
||||
data_destroy(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "delta.h"
|
||||
#include "data.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size == 0)
|
||||
return 0;
|
||||
|
||||
void* buf = malloc(size);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
Data* d = data_create(buf, size);
|
||||
if (!d)
|
||||
return 0;
|
||||
|
||||
DeltaSignature* sig = delta_signature_deserialize(d);
|
||||
if (sig)
|
||||
delta_signature_destroy(sig);
|
||||
|
||||
data_destroy(d);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "utils.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < 2)
|
||||
return 0;
|
||||
|
||||
// Split input into pattern and string at the midpoint
|
||||
size_t mid = size / 2;
|
||||
|
||||
char* pattern = malloc(mid + 1);
|
||||
char* str = malloc(size - mid + 1);
|
||||
if (!pattern || !str) {
|
||||
free(pattern);
|
||||
free(str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(pattern, data, mid);
|
||||
pattern[mid] = '\0';
|
||||
|
||||
memcpy(str, data + mid, size - mid);
|
||||
str[size - mid] = '\0';
|
||||
|
||||
glob_match(pattern, str);
|
||||
|
||||
free(pattern);
|
||||
free(str);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#include "metadata.h"
|
||||
#include "file.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < sizeof(int) + FILE_METADATA_WIRE_SIZE)
|
||||
return 0;
|
||||
|
||||
char* buf = malloc(size);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
|
||||
char* original_buf = buf;
|
||||
FileMetadata* m = metadata_from_buf(&buf);
|
||||
if (m)
|
||||
free(m);
|
||||
|
||||
free(original_buf);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import filecmp
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
BUILD_DIR = os.path.join(PROJECT_ROOT, "build")
|
||||
SERVER_CMD = [os.path.join(BUILD_DIR, "server")]
|
||||
CLIENT_CMD = [os.path.join(BUILD_DIR, "client")]
|
||||
TEST_DATA_DIR = os.path.join(PROJECT_ROOT, "test_data")
|
||||
|
||||
|
||||
class ServerManager:
|
||||
"""Manages a long-lived server process. Reuses across test cases."""
|
||||
|
||||
def __init__(self):
|
||||
self._proc = None
|
||||
self._port = None
|
||||
|
||||
def start(self, extra_args=None):
|
||||
self.stop()
|
||||
self._port = _find_free_port()
|
||||
cmd = SERVER_CMD + ["-p", str(self._port)]
|
||||
if extra_args:
|
||||
cmd += extra_args
|
||||
self._proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
_wait_for_port(self._port, timeout=5)
|
||||
|
||||
def stop(self):
|
||||
if self._proc:
|
||||
_wait_proc(self._proc)
|
||||
self._proc = None
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.stop()
|
||||
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
|
||||
|
||||
def run_client(source_dir, dest_dir, flags=None, port=None, extra_args=None):
|
||||
"""Run the client and return (result, duration)."""
|
||||
cmd = CLIENT_CMD + ["--source-dir", source_dir, "--dest-dir", dest_dir, "--save-to-disk"]
|
||||
if port:
|
||||
cmd += ["--server-port", str(port)]
|
||||
if flags:
|
||||
cmd += flags
|
||||
if extra_args:
|
||||
cmd += extra_args
|
||||
start = time.monotonic()
|
||||
result = subprocess.run(cmd, text=True, capture_output=True)
|
||||
duration = time.monotonic() - start
|
||||
return result, duration
|
||||
|
||||
|
||||
def run_client_posix(source_dir, dest_dir, flags=None, port=None):
|
||||
"""Run the client with positional args (rsync-style)."""
|
||||
cmd = CLIENT_CMD + [source_dir, dest_dir, "--save-to-disk"]
|
||||
if port:
|
||||
cmd += ["--server-port", str(port)]
|
||||
if flags:
|
||||
cmd += flags
|
||||
start = time.monotonic()
|
||||
result = subprocess.run(cmd, text=True, capture_output=True)
|
||||
duration = time.monotonic() - start
|
||||
return result, duration
|
||||
|
||||
|
||||
def generate_test_files(source_dir, full=False):
|
||||
"""Generate structured test data. Returns total bytes written."""
|
||||
if os.path.exists(source_dir):
|
||||
shutil.rmtree(source_dir)
|
||||
os.makedirs(source_dir)
|
||||
|
||||
target_total = 25 * 1024 * 1024 if full else 0
|
||||
written = 0
|
||||
|
||||
files = {
|
||||
"small.txt": b"hello world\n",
|
||||
"medium.txt": b"the quick brown fox jumps over the lazy dog\n" * 5000,
|
||||
"binary.bin": bytes(range(256)) * 1000,
|
||||
"nested/subdir/deep.txt": b"deeply nested file\n",
|
||||
"nested/another.txt": b"another nested file\n" * 50,
|
||||
}
|
||||
for rel_path, content in files.items():
|
||||
full_path = os.path.join(source_dir, rel_path)
|
||||
os.makedirs(os.path.dirname(full_path), exist_ok=True)
|
||||
with open(full_path, "wb") as f:
|
||||
f.write(content)
|
||||
written += len(content)
|
||||
|
||||
if full:
|
||||
os.makedirs(os.path.join(source_dir, "bulk"), exist_ok=True)
|
||||
i = 0
|
||||
while written < target_total:
|
||||
chunk_size = min(5 * 1024 * 1024, target_total - written)
|
||||
with open(os.path.join(source_dir, f"bulk/file_{i}.dat"), "wb") as f:
|
||||
f.write(random.randbytes(chunk_size))
|
||||
written += chunk_size
|
||||
i += 1
|
||||
|
||||
return written
|
||||
|
||||
|
||||
def verify_transfer(source_dir, received_dir):
|
||||
"""Verify all files from source exist in received_dir and match. Returns (mismatches, missing)."""
|
||||
source_dir = os.path.abspath(source_dir)
|
||||
received_dir = os.path.abspath(received_dir)
|
||||
if not os.path.exists(received_dir):
|
||||
return [], ["no received files found"]
|
||||
mismatches, missing = [], []
|
||||
for root, dirs, files in os.walk(source_dir):
|
||||
for f in files:
|
||||
src_path = os.path.join(root, f)
|
||||
rel = os.path.relpath(src_path, source_dir)
|
||||
dst_path = os.path.join(received_dir, rel)
|
||||
if not os.path.exists(dst_path):
|
||||
missing.append(rel)
|
||||
elif not filecmp.cmp(src_path, dst_path, shallow=False):
|
||||
mismatches.append(rel)
|
||||
return mismatches, missing
|
||||
|
||||
|
||||
def clean_dir(path):
|
||||
"""Remove and recreate a directory."""
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
|
||||
def make_result(name, success, duration=None, error=""):
|
||||
"""Create a standardized result dict."""
|
||||
return {
|
||||
"name": name,
|
||||
"status": "Success" if success else "Failed",
|
||||
"time": f"{duration:.4f}s" if duration is not None else "N/A",
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
def get_dest_received_dir(dest_dir, source_dir):
|
||||
"""Get the path where received files land inside dest_dir."""
|
||||
return os.path.join(dest_dir, os.path.abspath(source_dir).lstrip(os.sep))
|
||||
|
||||
|
||||
def _find_free_port():
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("", 0))
|
||||
return s.getsockname()[1]
|
||||
|
||||
|
||||
def _wait_for_port(port, timeout=5):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=0.3):
|
||||
return
|
||||
except (ConnectionRefusedError, OSError):
|
||||
time.sleep(0.05)
|
||||
raise RuntimeError(f"Server port {port} not ready after {timeout}s")
|
||||
|
||||
|
||||
def _wait_proc(proc, timeout=5):
|
||||
try:
|
||||
proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
@@ -0,0 +1,256 @@
|
||||
"""Feature tests: incremental sync, bandwidth limiting, dry run, metadata, filters."""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR,
|
||||
run_client,
|
||||
generate_test_files, verify_transfer, clean_dir, make_result,
|
||||
get_dest_received_dir, CLIENT_CMD,
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "feature_source")
|
||||
DEST_DIR = os.path.join(TEST_DATA_DIR, "feature_dest")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def setup_test_data():
|
||||
generate_test_files(SOURCE_DIR, full=False)
|
||||
clean_dir(DEST_DIR)
|
||||
yield
|
||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
class TestDryRun:
|
||||
def test_dry_run(self):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-n"],
|
||||
)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}"
|
||||
assert "Dry run:" in result.stdout, f"No dry run output: {result.stdout[:200]}"
|
||||
|
||||
|
||||
class TestArchiveMode:
|
||||
def test_archive_mode(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-a"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing: {missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
|
||||
|
||||
class TestExclude:
|
||||
def test_exclude_single(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--exclude", "small.txt"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert "small.txt" in missing, "small.txt should be excluded but was transferred"
|
||||
other_missing = [m for m in missing if m != "small.txt"]
|
||||
assert not other_missing, f"Other files missing: {other_missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
|
||||
def test_exclude_glob(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--exclude", "*.txt"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should be excluded"
|
||||
assert os.path.exists(os.path.join(received, "binary.bin")), "binary.bin should be present"
|
||||
|
||||
|
||||
class TestInclude:
|
||||
def test_include_single(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--include", "binary.bin"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
assert os.path.exists(os.path.join(received, "binary.bin")), "binary.bin should be included"
|
||||
assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should not be included"
|
||||
|
||||
def test_include_glob(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--include", "*.bin"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
assert os.path.exists(os.path.join(received, "binary.bin")), "binary.bin should be included"
|
||||
|
||||
|
||||
class TestSizeFilters:
|
||||
def test_max_size(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--max-size", "100"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
assert os.path.exists(os.path.join(received, "small.txt")), "small.txt should be present"
|
||||
assert not os.path.exists(os.path.join(received, "medium.txt")), "medium.txt should be skipped"
|
||||
|
||||
def test_min_size(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--min-size", "1000"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
assert not os.path.exists(os.path.join(received, "small.txt")), "small.txt should be skipped"
|
||||
assert os.path.exists(os.path.join(received, "medium.txt")), "medium.txt should be present"
|
||||
|
||||
|
||||
class TestIncremental:
|
||||
def test_incremental_skips_unchanged(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"First sync failed: {result.stderr[:100]}"
|
||||
|
||||
start = time.monotonic()
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
incremental_time = time.monotonic() - start
|
||||
|
||||
assert result.returncode == 0, f"Incremental sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing: {missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
|
||||
def test_incremental_detects_changes(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
modified_file = os.path.join(SOURCE_DIR, "small.txt")
|
||||
with open(modified_file, "wb") as f:
|
||||
f.write(b"modified content for incremental test\n")
|
||||
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
with open(modified_file, "wb") as f:
|
||||
f.write(b"hello world\n")
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
received_file = os.path.join(received, "small.txt")
|
||||
assert os.path.exists(received_file), "Modified file should be present"
|
||||
with open(received_file, "rb") as f:
|
||||
content = f.read()
|
||||
assert b"modified content" in content, f"Modified content not transferred: {content[:50]}"
|
||||
|
||||
|
||||
class TestDelete:
|
||||
def test_delete_removes_extra_files(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
extra_file = os.path.join(received, "extra_file.txt")
|
||||
extra_dir = os.path.join(received, "extra_dir")
|
||||
with open(extra_file, "w") as f:
|
||||
f.write("should be deleted")
|
||||
os.makedirs(extra_dir, exist_ok=True)
|
||||
with open(os.path.join(extra_dir, "nested.txt"), "w") as f:
|
||||
f.write("nested extra")
|
||||
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--delete"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
|
||||
assert result.returncode == 0, f"Delete sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
assert not os.path.exists(extra_file), "extra_file.txt should be deleted"
|
||||
assert not os.path.exists(extra_dir), "extra_dir should be deleted"
|
||||
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing: {missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
|
||||
|
||||
class TestProgress:
|
||||
def test_progress_output(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--progress"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}"
|
||||
output = result.stdout + result.stderr
|
||||
assert len(output) >= 0
|
||||
|
||||
|
||||
class TestBandwidthLimit:
|
||||
def test_bwlimit_runs(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--bwlimit", "10240"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}"
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing: {missing}"
|
||||
assert not mismatches, f"Mismatch: {mismatches}"
|
||||
@@ -0,0 +1,81 @@
|
||||
"""CLI validation and preflight checks."""
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import BUILD_DIR, CLIENT_CMD, SERVER_CMD
|
||||
|
||||
|
||||
class TestHelp:
|
||||
def test_client_help(self):
|
||||
r = subprocess.run(CLIENT_CMD + ["--help"], capture_output=True, text=True)
|
||||
assert r.returncode == 0
|
||||
assert "Usage:" in r.stdout
|
||||
assert "SSH transport" in r.stdout
|
||||
|
||||
def test_server_help(self):
|
||||
r = subprocess.run(SERVER_CMD + ["--help"], capture_output=True, text=True)
|
||||
assert r.returncode == 0
|
||||
assert "Usage:" in r.stdout
|
||||
|
||||
|
||||
class TestSSHDetection:
|
||||
def test_remote_dest_detected(self):
|
||||
"""Posix-style SSH dest should be detected and fail gracefully."""
|
||||
r = subprocess.run(
|
||||
CLIENT_CMD + ["/x", "somehost:/y"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
assert r.returncode != 0
|
||||
stderr = (r.stderr or "").lower()
|
||||
assert "ssh" in stderr or "error" in stderr or "could not" in stderr
|
||||
|
||||
def test_local_dest_not_ssh(self):
|
||||
"""Local path should not be detected as SSH."""
|
||||
r = subprocess.run(
|
||||
CLIENT_CMD + ["/tmp/x", "/tmp/y"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
# Should fail with connection error (no server), not SSH error
|
||||
assert r.returncode != 0
|
||||
|
||||
|
||||
class TestServerStdio:
|
||||
def test_stdio_mode_starts(self):
|
||||
"""Server --stdio should start and wait for stdin."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
SERVER_CMD + ["--stdio"],
|
||||
capture_output=True, text=True, timeout=3,
|
||||
)
|
||||
# Should exit with error (no data on stdin) or timeout
|
||||
except subprocess.TimeoutExpired:
|
||||
pass # Expected: server waiting for stdin
|
||||
|
||||
|
||||
class TestServerPort:
|
||||
def test_invalid_port(self):
|
||||
"""Server should reject invalid port numbers."""
|
||||
r = subprocess.run(
|
||||
SERVER_CMD + ["-p", "99999"],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
assert r.returncode != 0
|
||||
|
||||
def test_default_port(self):
|
||||
"""Server should start on default port 8080."""
|
||||
proc = subprocess.Popen(
|
||||
SERVER_CMD, stdout=subprocess.DEVNULL, stderr=None,
|
||||
)
|
||||
try:
|
||||
import socket, time
|
||||
time.sleep(0.5)
|
||||
with socket.create_connection(("127.0.0.1", 8080), timeout=2):
|
||||
pass # Port is listening
|
||||
except (ConnectionRefusedError, OSError):
|
||||
pytest.fail("Server not listening on default port 8080")
|
||||
finally:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=5)
|
||||
@@ -0,0 +1,132 @@
|
||||
"""SSH transport tests."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR,
|
||||
CLIENT_CMD, generate_test_files, verify_transfer, clean_dir, make_result,
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "ssh_source")
|
||||
DEST_DIR = os.path.join(TEST_DATA_DIR, "ssh_dest")
|
||||
SSH_AVAILABLE = False
|
||||
|
||||
|
||||
def _check_ssh():
|
||||
global SSH_AVAILABLE
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=5",
|
||||
"localhost", "which", "fastsync-server"],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
SSH_AVAILABLE = True
|
||||
return
|
||||
|
||||
# Try to install server binary into PATH
|
||||
server_path = os.path.join(BUILD_DIR, "server")
|
||||
r = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", "localhost", 'echo "$PATH"'],
|
||||
capture_output=True, timeout=10, text=True,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return
|
||||
for d in r.stdout.strip().split(":"):
|
||||
d = d.strip()
|
||||
if not d or "wrappers" in d:
|
||||
continue
|
||||
test = subprocess.run(
|
||||
["ssh", "-o", "BatchMode=yes", "localhost",
|
||||
f'test -w "{d}" && ln -sf {server_path} "{d}/fastsync-server" && which fastsync-server'],
|
||||
capture_output=True, timeout=10,
|
||||
)
|
||||
if test.returncode == 0:
|
||||
SSH_AVAILABLE = True
|
||||
return
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def setup_test_data():
|
||||
_check_ssh()
|
||||
if SSH_AVAILABLE:
|
||||
generate_test_files(SOURCE_DIR, full=False)
|
||||
clean_dir(DEST_DIR)
|
||||
yield
|
||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
def _run_ssh_test(name, flags, expected_missing=None):
|
||||
"""Run an SSH test case (no server process needed, client spawns SSH)."""
|
||||
ssh_dest = f"localhost:{DEST_DIR}"
|
||||
clean_dir(DEST_DIR)
|
||||
cmd = CLIENT_CMD + [SOURCE_DIR, ssh_dest, "--save-to-disk"] + flags
|
||||
start = __import__("time").monotonic()
|
||||
result = subprocess.run(cmd, text=True, capture_output=True)
|
||||
duration = __import__("time").monotonic() - start
|
||||
|
||||
if result.returncode != 0:
|
||||
return make_result(name, False, duration, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:100]}")
|
||||
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, DEST_DIR)
|
||||
if expected_missing:
|
||||
missing = [m for m in missing if m not in expected_missing]
|
||||
if missing:
|
||||
return make_result(name, False, duration, f"Missing: {', '.join(missing[:5])}")
|
||||
if mismatches:
|
||||
return make_result(name, False, duration, f"Mismatch: {', '.join(mismatches[:3])}")
|
||||
return make_result(name, True, duration)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SSH_AVAILABLE, reason="SSH to localhost not available")
|
||||
class TestSSHStandard:
|
||||
def test_standard(self):
|
||||
r = _run_ssh_test("SSH (localhost)", [])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithreading(self):
|
||||
r = _run_ssh_test("SSH Multithreading (-m)", ["-m"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression(self):
|
||||
r = _run_ssh_test("SSH Compression (-c)", ["-c"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_chunk_serialization(self):
|
||||
r = _run_ssh_test("SSH Chunk Serialization (-s)", ["-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression_chunk(self):
|
||||
r = _run_ssh_test("SSH Compression + Chunk (-c -s)", ["-c", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_compression(self):
|
||||
r = _run_ssh_test("SSH Multithread + Compression (-m -c)", ["-m", "-c"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_chunk(self):
|
||||
r = _run_ssh_test("SSH Multithread + Chunk (-m -s)", ["-m", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_all_flags(self):
|
||||
r = _run_ssh_test("SSH All Flags (-m -c -s)", ["-m", "-c", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not SSH_AVAILABLE, reason="SSH to localhost not available")
|
||||
class TestSSHFeatures:
|
||||
def test_archive(self):
|
||||
r = _run_ssh_test("SSH Archive (-a)", ["-a"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_exclude(self):
|
||||
r = _run_ssh_test("SSH Exclude (--exclude small.txt)",
|
||||
["--exclude", "small.txt"],
|
||||
expected_missing=["small.txt"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
@@ -0,0 +1,110 @@
|
||||
"""TCP transport correctness tests."""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
PROJECT_ROOT, BUILD_DIR, TEST_DATA_DIR,
|
||||
run_client, run_client_posix,
|
||||
generate_test_files, verify_transfer, clean_dir, make_result,
|
||||
get_dest_received_dir, CLIENT_CMD,
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "tcp_source")
|
||||
DEST_DIR = os.path.join(TEST_DATA_DIR, "tcp_dest")
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def setup_test_data():
|
||||
generate_test_files(SOURCE_DIR, full=False)
|
||||
clean_dir(DEST_DIR)
|
||||
yield
|
||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
def _run_tcp_test(name, port, flags, use_metadata=True, posix=False):
|
||||
"""Run a single TCP test case against a shared server."""
|
||||
clean_dir(DEST_DIR)
|
||||
if posix:
|
||||
result, dur = run_client_posix(SOURCE_DIR, DEST_DIR,
|
||||
flags=(["-M"] if use_metadata else []) + flags,
|
||||
port=port)
|
||||
else:
|
||||
result, dur = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=(["-M"] if use_metadata else []) + flags,
|
||||
port=port)
|
||||
|
||||
if result.returncode != 0:
|
||||
return make_result(name, False, dur, f"Exit {result.returncode}: {(result.stderr or result.stdout)[:100]}")
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
if missing:
|
||||
return make_result(name, False, dur, f"Missing: {', '.join(missing[:5])}")
|
||||
if mismatches:
|
||||
return make_result(name, False, dur, f"Mismatch: {', '.join(mismatches[:3])}")
|
||||
return make_result(name, True, dur)
|
||||
|
||||
|
||||
class TestTCPStandard:
|
||||
def test_standard(self, shared_server):
|
||||
r = _run_tcp_test("Standard", shared_server.port, [])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_posix_args(self, shared_server):
|
||||
r = _run_tcp_test("Posix Args", shared_server.port, [], posix=True)
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_no_metadata(self, shared_server):
|
||||
r = _run_tcp_test("Standard (no metadata)", shared_server.port, [], use_metadata=False)
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
class TestTCPFlags:
|
||||
def test_multithreading(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading (-m)", shared_server.port, ["-m"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression(self, shared_server):
|
||||
r = _run_tcp_test("Compression (-c)", shared_server.port, ["-c"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_chunk_serialization(self, shared_server):
|
||||
r = _run_tcp_test("Chunk Serialization (-s)", shared_server.port, ["-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression_chunk(self, shared_server):
|
||||
r = _run_tcp_test("Compression + Chunk (-c -s)", shared_server.port, ["-c", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_compression(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading + Compression (-m -c)", shared_server.port, ["-m", "-c"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_chunk(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading + Chunk (-m -s)", shared_server.port, ["-m", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_all_flags(self, shared_server):
|
||||
r = _run_tcp_test("Multithread + Compression + Chunk (-m -c -s)", shared_server.port, ["-m", "-c", "-s"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_sendfile(self, shared_server):
|
||||
r = _run_tcp_test("Sendfile (-f)", shared_server.port, ["-f"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_sendfile_multithread(self, shared_server):
|
||||
r = _run_tcp_test("Sendfile + Multithreading (-f -m)", shared_server.port, ["-f", "-m"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
class TestTCPChunkSize:
|
||||
def test_custom_chunk_size(self, shared_server):
|
||||
r = _run_tcp_test("Chunk size 5MB", shared_server.port, ["--chunk-size", "5242880"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_small_chunk_size(self, shared_server):
|
||||
r = _run_tcp_test("Chunk size 1KB", shared_server.port, ["--chunk-size", "1024"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
@@ -0,0 +1,181 @@
|
||||
"""TLS transport tests. Generates self-signed certs for testing."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import (
|
||||
PROJECT_ROOT, BUILD_DIR, SERVER_CMD, TEST_DATA_DIR,
|
||||
ServerManager, run_client,
|
||||
generate_test_files, verify_transfer, clean_dir, make_result,
|
||||
get_dest_received_dir, _find_free_port, _wait_proc,
|
||||
)
|
||||
|
||||
SOURCE_DIR = os.path.join(TEST_DATA_DIR, "tls_source")
|
||||
DEST_DIR = os.path.join(TEST_DATA_DIR, "tls_dest")
|
||||
CERT_DIR = os.path.join(TEST_DATA_DIR, "tls_certs")
|
||||
|
||||
|
||||
def _generate_certs(cert_dir):
|
||||
"""Generate a self-signed CA, server cert, and client cert for testing."""
|
||||
os.makedirs(cert_dir, exist_ok=True)
|
||||
ca_key = os.path.join(cert_dir, "ca.key")
|
||||
ca_cert = os.path.join(cert_dir, "ca.pem")
|
||||
server_key = os.path.join(cert_dir, "server.key")
|
||||
server_cert = os.path.join(cert_dir, "server.pem")
|
||||
client_key = os.path.join(cert_dir, "client.key")
|
||||
client_cert = os.path.join(cert_dir, "client.pem")
|
||||
|
||||
# CA key + cert
|
||||
subprocess.run([
|
||||
"openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", ca_key, "-out", ca_cert,
|
||||
"-days", "1", "-subj", "/CN=FastSync Test CA",
|
||||
], check=True, capture_output=True)
|
||||
|
||||
# Server key + CSR + cert (signed by CA)
|
||||
subprocess.run([
|
||||
"openssl", "req", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", server_key, "-out", os.path.join(cert_dir, "server.csr"),
|
||||
"-subj", "/CN=localhost",
|
||||
], check=True, capture_output=True)
|
||||
subprocess.run([
|
||||
"openssl", "x509", "-req", "-in", os.path.join(cert_dir, "server.csr"),
|
||||
"-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial",
|
||||
"-out", server_cert, "-days", "1",
|
||||
], check=True, capture_output=True)
|
||||
|
||||
# Client key + CSR + cert (signed by CA)
|
||||
subprocess.run([
|
||||
"openssl", "req", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", client_key, "-out", os.path.join(cert_dir, "client.csr"),
|
||||
"-subj", "/CN=fastsync-client",
|
||||
], check=True, capture_output=True)
|
||||
subprocess.run([
|
||||
"openssl", "x509", "-req", "-in", os.path.join(cert_dir, "client.csr"),
|
||||
"-CA", ca_cert, "-CAkey", ca_key, "-CAcreateserial",
|
||||
"-out", client_cert, "-days", "1",
|
||||
], check=True, capture_output=True)
|
||||
|
||||
return {
|
||||
"ca": ca_cert,
|
||||
"server_cert": server_cert,
|
||||
"server_key": server_key,
|
||||
"client_cert": client_cert,
|
||||
"client_key": client_key,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def certs():
|
||||
"""Generate test certificates once per test module."""
|
||||
if os.path.exists(CERT_DIR):
|
||||
shutil.rmtree(CERT_DIR)
|
||||
c = _generate_certs(CERT_DIR)
|
||||
yield c
|
||||
shutil.rmtree(CERT_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def setup_test_data():
|
||||
generate_test_files(SOURCE_DIR, full=False)
|
||||
clean_dir(DEST_DIR)
|
||||
yield
|
||||
shutil.rmtree(TEST_DATA_DIR, ignore_errors=True)
|
||||
|
||||
|
||||
class TestTLSBasic:
|
||||
def test_tls_server_client(self, certs):
|
||||
"""Basic TLS: server with cert/key, client with cert/key + CA."""
|
||||
clean_dir(DEST_DIR)
|
||||
with ServerManager() as server:
|
||||
server.start(extra_args=[
|
||||
"--tls", "--cert", certs["server_cert"], "--key", certs["server_key"],
|
||||
])
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--tls",
|
||||
"--cert", certs["client_cert"], "--key", certs["client_key"],
|
||||
"--ca", certs["ca"]],
|
||||
port=server.port,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing files: {missing}"
|
||||
assert not mismatches, f"Mismatched files: {mismatches}"
|
||||
|
||||
def test_tls_with_compression(self, certs):
|
||||
"""TLS + compression."""
|
||||
clean_dir(DEST_DIR)
|
||||
with ServerManager() as server:
|
||||
server.start(extra_args=[
|
||||
"--tls", "--cert", certs["server_cert"], "--key", certs["server_key"],
|
||||
])
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-c", "--tls",
|
||||
"--cert", certs["client_cert"], "--key", certs["client_key"],
|
||||
"--ca", certs["ca"]],
|
||||
port=server.port,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing files: {missing}"
|
||||
assert not mismatches, f"Mismatched files: {mismatches}"
|
||||
|
||||
def test_tls_with_multithreading(self, certs):
|
||||
"""TLS + multithreading."""
|
||||
clean_dir(DEST_DIR)
|
||||
with ServerManager() as server:
|
||||
server.start(extra_args=[
|
||||
"--tls", "--cert", certs["server_cert"], "--key", certs["server_key"],
|
||||
])
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-m", "--tls",
|
||||
"--cert", certs["client_cert"], "--key", certs["client_key"],
|
||||
"--ca", certs["ca"]],
|
||||
port=server.port,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
pytest.fail(f"Exit {result.returncode}: {(result.stderr or result.stdout)[:200]}")
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
mismatches, missing = verify_transfer(SOURCE_DIR, received)
|
||||
assert not missing, f"Missing files: {missing}"
|
||||
assert not mismatches, f"Mismatched files: {mismatches}"
|
||||
|
||||
|
||||
class TestTLSErrorCases:
|
||||
def test_server_tls_missing_cert_key(self):
|
||||
"""Server should fail if --tls is given without --cert/--key."""
|
||||
proc = subprocess.Popen(
|
||||
SERVER_CMD + ["--tls"],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE,
|
||||
)
|
||||
_, stderr = proc.communicate(timeout=5)
|
||||
assert proc.returncode != 0, "Server should fail with --tls but no cert/key"
|
||||
|
||||
def test_client_tls_missing_key(self):
|
||||
"""Client should fail if --tls is given without --key."""
|
||||
clean_dir(DEST_DIR)
|
||||
with ServerManager() as server:
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--tls",
|
||||
"--cert", "/nonexistent/cert.pem"],
|
||||
port=server.port,
|
||||
)
|
||||
assert result.returncode != 0, "Client should fail with --tls but no --key"
|
||||
@@ -1,8 +1,25 @@
|
||||
#include "test_array_list.h"
|
||||
#include "test_chunk.h"
|
||||
#include "test_compression.h"
|
||||
#include "test_config.h"
|
||||
#include "test_data.h"
|
||||
#include "test_delta.h"
|
||||
#include "test_file.h"
|
||||
#include "test_file_sendfile.h"
|
||||
#include "test_glob.h"
|
||||
#include "test_log.h"
|
||||
#include "test_metadata.h"
|
||||
#include "test_multiprocessing.h"
|
||||
#include "test_property.h"
|
||||
#include "test_protocol.h"
|
||||
#include "test_queue.h"
|
||||
#include "test_robustness.h"
|
||||
#include "test_scanner.h"
|
||||
#include "test_shared_utils.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 <stdio.h>
|
||||
|
||||
@@ -19,6 +36,23 @@ int main() {
|
||||
RUN_TEST(test_shared_utils);
|
||||
RUN_TEST(test_chunk);
|
||||
RUN_TEST(test_config);
|
||||
RUN_TEST(test_compression);
|
||||
RUN_TEST(test_scanner);
|
||||
RUN_TEST(test_delta);
|
||||
RUN_TEST(test_data);
|
||||
RUN_TEST(test_protocol);
|
||||
RUN_TEST(test_metadata);
|
||||
RUN_TEST(test_glob);
|
||||
RUN_TEST(test_file);
|
||||
RUN_TEST(test_file_sendfile);
|
||||
RUN_TEST(test_multiprocessing);
|
||||
RUN_TEST(test_log);
|
||||
RUN_TEST(test_transport_tcp);
|
||||
RUN_TEST(test_transport_ssh);
|
||||
RUN_TEST(test_transport_tls);
|
||||
RUN_TEST(test_robustness);
|
||||
RUN_TEST(test_stress);
|
||||
RUN_TEST(test_property);
|
||||
|
||||
printf("\n\033[1;36m=== TEST SUMMARY ===\033[0m\n");
|
||||
printf("Total Tests Run: %d\n", tests_run);
|
||||
|
||||
@@ -49,16 +49,4 @@ void test_array_list() {
|
||||
list->item_destroyer = test_destroyer;
|
||||
array_list_delete(list);
|
||||
EXPECT_EQ_INT(destroyer_calls, 106);
|
||||
|
||||
// Test clear with NULL destroyer
|
||||
list = array_list_create(NULL);
|
||||
int a = 1, b = 2;
|
||||
array_list_add(list, &a);
|
||||
array_list_add(list, &b);
|
||||
EXPECT_EQ_INT(list->size, 2);
|
||||
array_list_clear(list);
|
||||
EXPECT_EQ_INT(list->size, 0);
|
||||
EXPECT_NULL(list->items[0]);
|
||||
EXPECT_NULL(list->items[1]);
|
||||
array_list_delete(list);
|
||||
}
|
||||
|
||||
+32
-83
@@ -13,43 +13,27 @@ static void test_file_operations() {
|
||||
|
||||
to_disk(test_path, test_content, test_len);
|
||||
|
||||
struct stat st;
|
||||
int stat_res = stat(test_path, &st);
|
||||
EXPECT_EQ_INT(stat_res, 0);
|
||||
EXPECT_EQ_INT((int)st.st_size, (int)test_len);
|
||||
|
||||
File *f = file_create(test_path, &st);
|
||||
File* f = file_create(test_path);
|
||||
EXPECT_NOT_NULL(f);
|
||||
EXPECT_EQ_STR(f->path, test_path);
|
||||
EXPECT_NULL(f->data);
|
||||
EXPECT_NOT_NULL(f->data);
|
||||
EXPECT_NULL(f->data->data);
|
||||
EXPECT_EQ_INT((int)f->data->size, 0);
|
||||
|
||||
struct stat st;
|
||||
stat(test_path, &st);
|
||||
f->data->size = st.st_size;
|
||||
|
||||
file_load_data(f);
|
||||
EXPECT_NOT_NULL(f->data);
|
||||
EXPECT_EQ_INT(memcmp(f->data, test_content, test_len), 0);
|
||||
EXPECT_NOT_NULL(f->data->data);
|
||||
EXPECT_EQ_INT((int)f->data->size, (int)test_len);
|
||||
EXPECT_EQ_INT(memcmp(f->data->data, test_content, test_len), 0);
|
||||
|
||||
file_destroy(f);
|
||||
unlink(test_path);
|
||||
}
|
||||
|
||||
static void test_file_receive_operations() {
|
||||
char *path = str_dup("temp_receive.txt");
|
||||
char *data = str_dup("receive data content");
|
||||
unsigned long long size = strlen(data);
|
||||
|
||||
Data *df = data_create(data, size);
|
||||
EXPECT_NOT_NULL(df);
|
||||
EXPECT_EQ_INT((int)df->size, (int)size);
|
||||
EXPECT_EQ_STR(df->data, "receive data content");
|
||||
|
||||
FileReceive *fr = file_receive_create(path, df);
|
||||
EXPECT_NOT_NULL(fr);
|
||||
EXPECT_EQ_STR(fr->path, "temp_receive.txt");
|
||||
EXPECT_NOT_NULL(fr->data);
|
||||
EXPECT_EQ_STR(fr->data->data, "receive data content");
|
||||
|
||||
file_receive_destroy(fr);
|
||||
}
|
||||
|
||||
static void test_chunk_operations() {
|
||||
char* path1 = "temp_chunk_1.txt";
|
||||
char* content1 = "chunk item 1";
|
||||
@@ -66,8 +50,10 @@ static void test_chunk_operations() {
|
||||
stat(path1, &st1);
|
||||
stat(path2, &st2);
|
||||
|
||||
File *f1 = file_create(path1, &st1);
|
||||
File *f2 = file_create(path2, &st2);
|
||||
File* f1 = file_create(path1);
|
||||
f1->data->size = st1.st_size;
|
||||
File* f2 = file_create(path2);
|
||||
f2->data->size = st2.st_size;
|
||||
|
||||
File* files[2] = {f1, f2};
|
||||
Chunk* chunk = chunk_create(files, 2);
|
||||
@@ -76,63 +62,27 @@ static void test_chunk_operations() {
|
||||
EXPECT_NOT_NULL(chunk->items[0]);
|
||||
EXPECT_NOT_NULL(chunk->items[1]);
|
||||
|
||||
Data *formatted = chunk_format(chunk);
|
||||
EXPECT_NOT_NULL(formatted);
|
||||
// load data before serializing
|
||||
file_load_data(f1);
|
||||
file_load_data(f2);
|
||||
|
||||
unsigned long long expected_size =
|
||||
(sizeof(int) + strlen(path1) + sizeof(unsigned long long) + len1) +
|
||||
(sizeof(int) + strlen(path2) + sizeof(unsigned long long) + len2);
|
||||
EXPECT_EQ_INT((int)formatted->size, (int)expected_size);
|
||||
// Test chunk_serialize / chunk_deserialize round-trip
|
||||
Data* serialized = chunk_serialize(chunk, false);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
char *ptr = (char *)formatted->data;
|
||||
Chunk* deserialized = chunk_deserialize(serialized, false);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT(deserialized->element_count, 2);
|
||||
EXPECT_EQ_STR(deserialized->items[0]->path, path1);
|
||||
EXPECT_EQ_STR(deserialized->items[1]->path, path2);
|
||||
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, (int)len1);
|
||||
EXPECT_EQ_INT((int)deserialized->items[1]->data->size, (int)len2);
|
||||
EXPECT_EQ_INT(memcmp(deserialized->items[0]->data->data, content1, len1), 0);
|
||||
EXPECT_EQ_INT(memcmp(deserialized->items[1]->data->data, content2, len2), 0);
|
||||
|
||||
// File 1
|
||||
int p_len1;
|
||||
memcpy(&p_len1, ptr, sizeof(int));
|
||||
ptr += sizeof(int);
|
||||
EXPECT_EQ_INT(p_len1, (int)strlen(path1));
|
||||
data_destroy(serialized);
|
||||
chunk_destroy(deserialized);
|
||||
|
||||
char read_path1[256];
|
||||
memcpy(read_path1, ptr, p_len1);
|
||||
read_path1[p_len1] = '\0';
|
||||
ptr += p_len1;
|
||||
EXPECT_EQ_STR(read_path1, path1);
|
||||
|
||||
unsigned long long d_len1;
|
||||
memcpy(&d_len1, ptr, sizeof(unsigned long long));
|
||||
ptr += sizeof(unsigned long long);
|
||||
EXPECT_EQ_INT((int)d_len1, (int)len1);
|
||||
|
||||
char read_content1[256];
|
||||
memcpy(read_content1, ptr, d_len1);
|
||||
read_content1[d_len1] = '\0';
|
||||
ptr += d_len1;
|
||||
EXPECT_EQ_STR(read_content1, content1);
|
||||
|
||||
// File 2
|
||||
int p_len2;
|
||||
memcpy(&p_len2, ptr, sizeof(int));
|
||||
ptr += sizeof(int);
|
||||
EXPECT_EQ_INT(p_len2, (int)strlen(path2));
|
||||
|
||||
char read_path2[256];
|
||||
memcpy(read_path2, ptr, p_len2);
|
||||
read_path2[p_len2] = '\0';
|
||||
ptr += p_len2;
|
||||
EXPECT_EQ_STR(read_path2, path2);
|
||||
|
||||
unsigned long long d_len2;
|
||||
memcpy(&d_len2, ptr, sizeof(unsigned long long));
|
||||
ptr += sizeof(unsigned long long);
|
||||
EXPECT_EQ_INT((int)d_len2, (int)len2);
|
||||
|
||||
char read_content2[256];
|
||||
memcpy(read_content2, ptr, d_len2);
|
||||
read_content2[d_len2] = '\0';
|
||||
ptr += d_len2;
|
||||
EXPECT_EQ_STR(read_content2, content2);
|
||||
|
||||
chunk_data_delete(formatted);
|
||||
chunk_destroy(chunk);
|
||||
|
||||
unlink(path1);
|
||||
@@ -141,6 +91,5 @@ static void test_chunk_operations() {
|
||||
|
||||
void test_chunk() {
|
||||
test_file_operations();
|
||||
test_file_receive_operations();
|
||||
test_chunk_operations();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "test_utils.h"
|
||||
#include "chunk.h"
|
||||
#include "compression.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "utils.h"
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_data_compress_decompress_roundtrip() {
|
||||
const char original[] = "Hello, World! This is test data for compression round-trip!";
|
||||
size_t len = strlen(original);
|
||||
|
||||
char* buf = malloc(len);
|
||||
memcpy(buf, original, len);
|
||||
Data* original_data = data_create(buf, len);
|
||||
EXPECT_NOT_NULL(original_data);
|
||||
|
||||
Data* compressed = data_compress(original_data, 3);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed = data_decompress(compressed);
|
||||
EXPECT_NOT_NULL(decompressed);
|
||||
EXPECT_EQ_INT((int)decompressed->size, (int)len);
|
||||
EXPECT_EQ_INT(memcmp(decompressed->data, original, len), 0);
|
||||
|
||||
data_destroy(original_data);
|
||||
data_destroy(compressed);
|
||||
data_destroy(decompressed);
|
||||
}
|
||||
|
||||
static void test_data_compress_decompress_large() {
|
||||
size_t size = 1024 * 10;
|
||||
char* original = malloc(size);
|
||||
EXPECT_NOT_NULL(original);
|
||||
for (size_t i = 0; i < size; i++)
|
||||
original[i] = (char)(i % 256);
|
||||
|
||||
Data* original_data = data_create(original, size);
|
||||
EXPECT_NOT_NULL(original_data);
|
||||
|
||||
Data* compressed = data_compress(original_data, 1);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed = data_decompress(compressed);
|
||||
EXPECT_NOT_NULL(decompressed);
|
||||
EXPECT_EQ_INT((int)decompressed->size, (int)size);
|
||||
EXPECT_EQ_INT(memcmp(decompressed->data, original, size), 0);
|
||||
|
||||
data_destroy(original_data);
|
||||
data_destroy(compressed);
|
||||
data_destroy(decompressed);
|
||||
}
|
||||
|
||||
static void test_chunk_compress_decompress_roundtrip() {
|
||||
char* path1 = "temp_comp_test_1.txt";
|
||||
char* content1 = "chunk compression test file 1";
|
||||
unsigned long long len1 = strlen(content1);
|
||||
|
||||
char* path2 = "temp_comp_test_2.txt";
|
||||
char* content2 = "chunk compression test file 2 with more data";
|
||||
unsigned long long len2 = strlen(content2);
|
||||
|
||||
to_disk(path1, content1, len1);
|
||||
to_disk(path2, content2, len2);
|
||||
|
||||
struct stat st1, st2;
|
||||
EXPECT_EQ_INT(stat(path1, &st1), 0);
|
||||
EXPECT_EQ_INT(stat(path2, &st2), 0);
|
||||
|
||||
File* f1 = file_create(path1);
|
||||
f1->data->size = st1.st_size;
|
||||
File* f2 = file_create(path2);
|
||||
f2->data->size = st2.st_size;
|
||||
EXPECT_NOT_NULL(f1);
|
||||
EXPECT_NOT_NULL(f2);
|
||||
|
||||
file_load_data(f1);
|
||||
file_load_data(f2);
|
||||
|
||||
File* files[2] = {f1, f2};
|
||||
Chunk* chunk = chunk_create(files, 2);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
|
||||
Data* compressed = chunk_compress(chunk, 3, false);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed_data = data_decompress(compressed);
|
||||
EXPECT_NOT_NULL(decompressed_data);
|
||||
|
||||
Chunk* decompressed_chunk = chunk_deserialize(decompressed_data, false);
|
||||
EXPECT_NOT_NULL(decompressed_chunk);
|
||||
EXPECT_EQ_INT(decompressed_chunk->element_count, 2);
|
||||
|
||||
EXPECT_EQ_STR(decompressed_chunk->items[0]->path, path1);
|
||||
EXPECT_EQ_INT((int)decompressed_chunk->items[0]->data->size, (int)len1);
|
||||
EXPECT_EQ_INT(memcmp(decompressed_chunk->items[0]->data->data, content1, len1), 0);
|
||||
|
||||
EXPECT_EQ_STR(decompressed_chunk->items[1]->path, path2);
|
||||
EXPECT_EQ_INT((int)decompressed_chunk->items[1]->data->size, (int)len2);
|
||||
EXPECT_EQ_INT(memcmp(decompressed_chunk->items[1]->data->data, content2, len2), 0);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
data_destroy(compressed);
|
||||
data_destroy(decompressed_data);
|
||||
chunk_destroy(decompressed_chunk);
|
||||
|
||||
unlink(path1);
|
||||
unlink(path2);
|
||||
}
|
||||
|
||||
void test_compression() {
|
||||
test_data_compress_decompress_roundtrip();
|
||||
test_data_compress_decompress_large();
|
||||
test_chunk_compress_decompress_roundtrip();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef TEST_COMPRESSION_H
|
||||
#define TEST_COMPRESSION_H
|
||||
|
||||
void test_compression();
|
||||
|
||||
#endif
|
||||
+46
-7
@@ -7,8 +7,8 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
static void test_config_lifecycle() {
|
||||
Config *cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/dst"),
|
||||
true, true, false, false, 1, 4);
|
||||
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_EQ_STR(cfg->version, "1.0");
|
||||
EXPECT_EQ_STR(cfg->send_directory, "/src");
|
||||
@@ -17,13 +17,49 @@ static void test_config_lifecycle() {
|
||||
EXPECT_TRUE(cfg->use_multithreading);
|
||||
EXPECT_FALSE(cfg->use_chunk_serialization);
|
||||
EXPECT_FALSE(cfg->use_compression);
|
||||
EXPECT_EQ_INT(cfg->num_connections, 4);
|
||||
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
||||
EXPECT_NULL(cfg->ssh_destination);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_config_ssh_dest() {
|
||||
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_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
||||
EXPECT_NULL(cfg->ssh_destination);
|
||||
EXPECT_EQ_STR(cfg->receive_root_directory, "user@host:/dst");
|
||||
|
||||
config_parse_ssh_dest(cfg);
|
||||
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
|
||||
EXPECT_EQ_STR(cfg->ssh_destination, "user@host:/dst");
|
||||
EXPECT_EQ_STR(cfg->receive_root_directory, "/dst");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_config_ssh_dest_local_path() {
|
||||
Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/local/path"), true, false,
|
||||
false, false, false, 1, false, 0);
|
||||
config_parse_ssh_dest(cfg);
|
||||
EXPECT_EQ_INT(cfg->transport, TRANSPORT_TCP);
|
||||
EXPECT_NULL(cfg->ssh_destination);
|
||||
EXPECT_EQ_STR(cfg->receive_root_directory, "/local/path");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_config_ssh_dest_no_user() {
|
||||
Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("host:/remote"), true, false,
|
||||
false, false, false, 1, false, 0);
|
||||
config_parse_ssh_dest(cfg);
|
||||
EXPECT_EQ_INT(cfg->transport, TRANSPORT_SSH);
|
||||
EXPECT_EQ_STR(cfg->ssh_destination, "host:/remote");
|
||||
EXPECT_EQ_STR(cfg->receive_root_directory, "/remote");
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_pipeline_sender_lifecycle() {
|
||||
Config *cfg = config_create(str_dup("2.0"), str_dup("/src2"),
|
||||
str_dup("/dst2"), false, false, true, true, 1, 8);
|
||||
Config* cfg = config_create(str_dup("2.0"), str_dup("/src2"), str_dup("/dst2"), false, false,
|
||||
true, true, false, 1, false, 0);
|
||||
Queue* q1 = queue_create(5, NULL);
|
||||
Queue* q2 = queue_create(15, NULL);
|
||||
|
||||
@@ -39,8 +75,8 @@ static void test_pipeline_sender_lifecycle() {
|
||||
}
|
||||
|
||||
static void test_pipeline_receiver_lifecycle() {
|
||||
Config *cfg = config_create(str_dup("3.0"), str_dup("/src3"),
|
||||
str_dup("/dst3"), true, true, true, true, 1, 2);
|
||||
Config* cfg = config_create(str_dup("3.0"), str_dup("/src3"), str_dup("/dst3"), true, true, true,
|
||||
true, false, 1, false, 0);
|
||||
Queue* q = queue_create(20, NULL);
|
||||
|
||||
PipelineContextReceiver* pcr = pipeline_context_receiver_create(cfg, q, 42);
|
||||
@@ -55,6 +91,9 @@ static void test_pipeline_receiver_lifecycle() {
|
||||
|
||||
void test_config() {
|
||||
test_config_lifecycle();
|
||||
test_config_ssh_dest();
|
||||
test_config_ssh_dest_local_path();
|
||||
test_config_ssh_dest_no_user();
|
||||
test_pipeline_sender_lifecycle();
|
||||
test_pipeline_receiver_lifecycle();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "data.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void test_data_create() {
|
||||
char* buf = malloc(6);
|
||||
EXPECT_NOT_NULL(buf);
|
||||
memcpy(buf, "hello", 6);
|
||||
Data* d = data_create(buf, 6);
|
||||
EXPECT_NOT_NULL(d);
|
||||
EXPECT_NOT_NULL(d->data);
|
||||
EXPECT_TRUE(d->data == buf);
|
||||
EXPECT_EQ_INT((int)d->size, 6);
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_data_create_empty() {
|
||||
Data* d = data_create_empty(256);
|
||||
EXPECT_NOT_NULL(d);
|
||||
EXPECT_NOT_NULL(d->data);
|
||||
EXPECT_EQ_INT((int)d->size, 256);
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_data_create_empty_zero() {
|
||||
Data* d = data_create_empty(0);
|
||||
EXPECT_NOT_NULL(d);
|
||||
EXPECT_NOT_NULL(d->data);
|
||||
EXPECT_EQ_INT((int)d->size, 0);
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_data_create_reserve() {
|
||||
Data* d = data_create_reserve(1024);
|
||||
EXPECT_NOT_NULL(d);
|
||||
EXPECT_NULL(d->data);
|
||||
EXPECT_EQ_INT((int)d->size, 1024);
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_data_destroy_null() {
|
||||
data_destroy(NULL);
|
||||
}
|
||||
|
||||
static void test_data_destroy_normal() {
|
||||
Data* d = data_create_empty(128);
|
||||
EXPECT_NOT_NULL(d);
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
void test_data() {
|
||||
test_data_create();
|
||||
test_data_create_empty();
|
||||
test_data_create_empty_zero();
|
||||
test_data_create_reserve();
|
||||
test_data_destroy_null();
|
||||
test_data_destroy_normal();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user