Compare commits

..

7 Commits

Author SHA1 Message Date
TapTap 5c686b0655 fix: address PR review — doc accuracy, clang-tidy info, config leak fix
CI / lint (push) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (push) Successful in 54s
CI / clang-tidy (push) Successful in 6s
CI / sanitizers (address) (push) Successful in 59s
CI / build-and-test (pull_request) Successful in 55s
CI / clang-tidy (pull_request) Successful in 5s
CI / sanitizers (address) (pull_request) Successful in 57s
Review findings addressed:

Critical:
- cmake-expert.md: replace commented-out sanitizer lines with actual
  SANITIZER cache variable block, fix ENABLE_ASAN/TSAN/UBSAN pattern
- integrator.md: update YAML example to v7, use -DSANITIZER= instead
  of raw flags, add symlink + LSAN suppression steps

Warnings:
- ci.yaml: rename clang-tidy step to 'informational, non-blocking',
  add ::warning:: workflow command for visibility
- client_cli.c: add goto cleanup pattern to free config on error paths
  (skip for multithreaded path where pipeline_context_sender_destroy
  already owns config)

Suggestions:
- test-writer.md: clarify conftest.py is at tests/conftest.py
- .lsan-suppressions.txt: remove config_create suppression (leak fixed)
2026-07-20 14:28:42 +02:00
TapTap c7f34eaf9d ci: symlink build dir for sanitizer integration tests 2026-07-20 14:22:27 +02:00
TapTap bd17f50f9b feat: update AI automation agents, add reviewer agent, fix leaks, enhance CI
Agents:
- cmake-expert: fix stale CMake version (4.1 -> 3.22), add OpenSSL/xxHash deps
- integrator: fix stale test.py references to tests/integration/, update CI docs
- test-writer: update integration test patterns for modular test structure
- reviewer: new comprehensive PR reviewer (code, build, CI, docs, quality)

Bug fixes:
- delta.c: set instructions[i].type = DELTA_INSTR_LITERAL in deserialize
- scanner.c: free ArrayList when directory_scanner_next returns NULL

CI:
- Add sanitizer job (ASan + UBSan) with LSAN suppressions for pre-existing leaks
- Add clang-tidy job with proper warning/error detection
- Remove || true masking (fixes now make sanitizer useful)

Cleanup:
- gitignore build-asan/
- .lsan-suppressions.txt for known CLI config leaks
2026-07-20 14:22:27 +02:00
TapTap a4b35e136b Merge pull request 'docs: add AGENTS.md with custom-image dependency install rule' (#27) from feat/custom-image-deps-rule into main
CI / lint (push) Successful in 12s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (push) Successful in 53s
Reviewed-on: #27
2026-07-19 22:57:15 +02:00
TapTap 02ceb6828b fix: re-review cleanup — deps/conventions/sanitizer sections in cmake-expert, AGENTS.md notes
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 7s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (push) Successful in 54s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 22:54:02 +02:00
TapTap 0f9e689e39 fix: address PR review — lcov/valgrind in Dockerfile, correct cmake-expert.md, expand AGENTS.md
CI / lint (push) Successful in 8s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (push) Successful in 53s
CI / sanitizers (address) (push) Successful in 14s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 22:37:26 +02:00
TapTap a7bb6454a4 docs: add AGENTS.md with custom-image dependency install rule
CI / lint (push) Successful in 8s
CI / build-and-test (push) Successful in 52s
CI / sanitizers (address) (push) Successful in 13s
CI / lint (pull_request) Successful in 7s
CI / build-and-test (pull_request) Successful in 53s
CI / sanitizers (address) (pull_request) Successful in 14s
2026-07-19 20:43:54 +02:00
9 changed files with 162 additions and 92 deletions
+4 -2
View File
@@ -73,11 +73,13 @@ jobs:
- name: Configure (generate compile_commands.json) - name: Configure (generate compile_commands.json)
run: cmake -B build -S . run: cmake -B build -S .
- name: Run clang-tidy - name: Run clang-tidy (informational, non-blocking)
run: | run: |
find src/ -name '*.c' | xargs clang-tidy -p build \ find src/ -name '*.c' | xargs clang-tidy -p build \
--checks='-*,bugprone-*,clang-analyzer-*,misc-*,-misc-no-recursion' \ --checks='-*,bugprone-*,clang-analyzer-*,misc-*,-misc-no-recursion' \
2>&1 | tee clang-tidy-output.txt 2>&1 | tee clang-tidy-output.txt
if grep -q -E " error:| warning:" clang-tidy-output.txt; then if grep -q -E " error:| warning:" clang-tidy-output.txt; then
echo "clang-tidy found issues — review the output above" echo "::warning::clang-tidy found issues — review the output above"
else
echo "clang-tidy: no issues found"
fi fi
+5 -5
View File
@@ -1,6 +1,6 @@
# LSAN suppressions for FastSync # LSAN suppressions for FastSync
# Pre-existing leaks — not introduced by this PR. # Add suppression entries here for known pre-existing leaks that cannot be
# Remove these as each leak is fixed. # fixed immediately. Remove entries as leaks are fixed.
#
# Config object never freed at CLI exit (main allocates, OS reclaims) # Example format:
leak:config_create # leak:function_name
+2
View File
@@ -9,6 +9,8 @@ You are a system architect for the FastSync project — a high-performance file
Make high-level design decisions. Evaluate trade-offs, plan module interactions, design data flow, and ensure architectural coherence across the codebase. Make high-level design decisions. Evaluate trade-offs, plan module interactions, design data flow, and ensure architectural coherence across the codebase.
> **Environment rule:** dependency installation must always use the project's custom Docker image (repo-root `Dockerfile`, same as CI) — never ad-hoc host package installs. See `AGENTS.md`.
## Project Architecture ## Project Architecture
### Module Map ### Module Map
+40 -57
View File
@@ -21,18 +21,29 @@ set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
add_compile_options(-Wall -g -O3) add_compile_options(-Wall -g -O3)
# add_compile_options(-Wall -g -O1 -fsanitize=address)
# add_link_options(-fsanitize=address)
include(FetchContent) include(FetchContent)
FetchContent_Declare( FetchContent_Declare(xxhash GIT_REPOSITORY https://github.com/Cyan4973/xxHash GIT_TAG v0.8.3 SOURCE_SUBDIR cmake_unofficial)
xxhash
GIT_REPOSITORY https://github.com/Cyan4973/xxHash
GIT_TAG v0.8.3
SOURCE_SUBDIR cmake_unofficial
)
FetchContent_MakeAvailable(xxhash) 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) set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
@@ -43,13 +54,11 @@ endif()
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
# Source file collection
file(GLOB SHARED_SRCS "src/shared/*.c") file(GLOB SHARED_SRCS "src/shared/*.c")
file(GLOB SERVER_SRCS "src/server/*.c") file(GLOB SERVER_SRCS "src/server/*.c")
file(GLOB CLIENT_SRCS "src/client/*.c") file(GLOB CLIENT_SRCS "src/client/*.c")
file(GLOB TEST_SRCS "tests/*.c") file(GLOB TEST_SRCS "tests/*.c")
# Targets
add_executable(server ${SERVER_SRCS} ${SHARED_SRCS}) add_executable(server ${SERVER_SRCS} ${SHARED_SRCS})
target_include_directories(server PRIVATE src/shared src/server src/client) target_include_directories(server PRIVATE src/shared src/server src/client)
target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash) target_link_libraries(server PRIVATE Threads::Threads ${ZSTD_LIBRARY} OpenSSL::SSL OpenSSL::Crypto xxhash)
@@ -75,7 +84,7 @@ tests/integration/ — Python pytest integration tests
### Dependencies ### Dependencies
- **zstd** — found via `find_library(ZSTD_LIBRARY zstd)` - **zstd** — found via `find_library(ZSTD_LIBRARY zstd)`
- **OpenSSL** — found via `find_package(OpenSSL REQUIRED)` (TLS 1.2+ transport) - **OpenSSL** — found via `find_package(OpenSSL REQUIRED)` (TLS 1.2+ transport)
- **xxHash** — fetched via `FetchContent` from GitHub (delta transfer hashing) - **xxHash** — fetched via `FetchContent` from GitHub (delta transfer hashing, v0.8.3)
- **pthreads** — found via `find_package(Threads REQUIRED)` - **pthreads** — found via `find_package(Threads REQUIRED)`
- **C11 standard** — required - **C11 standard** — required
- **CMake 3.22+** — minimum version - **CMake 3.22+** — minimum version
@@ -83,10 +92,11 @@ tests/integration/ — Python pytest integration tests
## Conventions ## Conventions
- Use `file(GLOB ...)` for source collection (existing pattern). - Use `file(GLOB ...)` for source collection (existing pattern).
- All targets link `Threads::Threads` and `${ZSTD_LIBRARY}`. - 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). - Include directories: `src/shared`, `src/server`, `src/client`, `tests` (for test target).
- Sanitizer support is commented out but present (`-fsanitize=address`). - 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)`. - Build with `cmake -B build -S . && cmake --build build -j$(nproc)`.
- Install dependencies only via the project's custom Docker image (repo-root `Dockerfile`, same image CI uses) — never via host package installs; see `AGENTS.md`.
## When Making Changes ## When Making Changes
@@ -95,28 +105,21 @@ tests/integration/ — Python pytest integration tests
3. Add new dependencies with `find_package` or `find_library`. 3. Add new dependencies with `find_package` or `find_library`.
4. When adding a new executable target, follow the pattern of existing targets. 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. 5. When adding a new library (static/shared), use `add_library` and follow the project's naming.
6. For sanitizer builds, use the commented-out `-fsanitize=address` lines as reference. 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. 7. Always verify the build compiles after changes.
## Sanitizer Configurations ## Sanitizer Configurations
### AddressSanitizer (memory errors) Use the project's built-in `-DSANITIZER=` option (matching the CI matrix):
```bash ```bash
cmake -B build -S . \ cmake -B build -S . -DSANITIZER=address # AddressSanitizer (memory errors)
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \ cmake --build build -j$(nproc)
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake -B build -S . -DSANITIZER=thread # ThreadSanitizer (race conditions)
cmake --build build -j$(nproc) cmake --build build -j$(nproc)
``` ```
### ThreadSanitizer (race conditions) For UndefinedBehaviorSanitizer (no `-DSANITIZER=undefined` option in CMakeLists.txt yet), use the manual flag approach:
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=thread -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
cmake --build build -j$(nproc)
```
### UndefinedBehaviorSanitizer
```bash ```bash
cmake -B build -S . \ cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \ -DCMAKE_C_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
@@ -124,14 +127,6 @@ cmake -B build -S . \
cmake --build build -j$(nproc) cmake --build build -j$(nproc)
``` ```
### Combined Sanitizers
```bash
cmake -B build -S . \
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
cmake --build build -j$(nproc)
```
### Using ccache (faster rebuilds) ### Using ccache (faster rebuilds)
```bash ```bash
cmake -B build -S . -DCMAKE_C_COMPILER_LAUNCHER=ccache cmake -B build -S . -DCMAKE_C_COMPILER_LAUNCHER=ccache
@@ -169,31 +164,19 @@ cmake --build build -j$(nproc)
./build/tests ./build/tests
``` ```
## When Adding Sanitizer Support to CMakeLists.txt ## Sanitizer Integration
Use CMake options for cleaner integration: The project uses a single `SANITIZER` cache variable in `CMakeLists.txt`:
```cmake ```cmake
option(ENABLE_ASAN "Enable AddressSanitizer" OFF) set(SANITIZER "none" CACHE STRING "Sanitizer to enable (address, thread, none)")
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF) set_property(CACHE SANITIZER PROPERTY STRINGS address thread none)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_TSAN)
add_compile_options(-fsanitize=thread)
add_link_options(-fsanitize=thread)
endif()
if(ENABLE_UBSAN)
add_compile_options(-fsanitize=undefined)
add_link_options(-fsanitize=undefined)
endif()
``` ```
Supported values: `address`, `thread`, `none`. Unknown values trigger `FATAL_ERROR`.
Then build with: Build with:
```bash ```bash
cmake -B build -S . -DENABLE_ASAN=ON 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.
+12 -9
View File
@@ -115,18 +115,21 @@ The project uses Gitea Actions. Key jobs:
jobs: jobs:
sanitizer: sanitizer:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: gitea.tap-tap.win/taptap/fastsync-ci:v6 container: gitea.tap-tap.win/taptap/fastsync-ci:v7
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build with ASan + UBSan - name: Configure
run: | run: cmake -B build-${{ matrix.sanitizer }} -S . -DSANITIZER=${{ matrix.sanitizer }}
cmake -B build -S . \ - name: Build
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g" \ run: cmake --build build-${{ matrix.sanitizer }} -j$(nproc)
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" - name: Symlink for integration tests
cmake --build build -j$(nproc) run: ln -sf build-${{ matrix.sanitizer }} build
- name: Run tests - name: Unit Tests
run: ./build/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 ## Verification Checklist
+1 -1
View File
@@ -173,7 +173,7 @@ When writing integration tests (Python-based), follow the patterns in `tests/int
- `test_tls.py` — TLS transport tests - `test_tls.py` — TLS transport tests
- `test_features.py` — feature-specific tests (delete, exclude, incremental, etc.) - `test_features.py` — feature-specific tests (delete, exclude, incremental, etc.)
Use `conftest.py` fixtures for server setup/teardown. 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 ### Minimal Integration Test
```python ```python
+57
View File
@@ -0,0 +1,57 @@
# 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
**Rule: always install dependencies using the project's custom Docker image — never via ad-hoc system package installs on the host** (no `apt-get install` / `pip install` on the host machine).
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.
```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 image, add it to the `Dockerfile` (and rebuild) rather than installing it on the host.
## 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
```
+1 -1
View File
@@ -1,7 +1,7 @@
FROM ubuntu:24.04 FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends \ 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 \ 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 && \ python3 python3-pip python3-venv openssl openssh-client lcov valgrind && \
pip3 install --break-system-packages pytest && \ pip3 install --break-system-packages pytest && \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \ apt-get install -y --no-install-recommends nodejs && \
+40 -17
View File
@@ -73,6 +73,8 @@ int main(int argc, char* argv[]) {
Config* config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL, save_to_disk, false, false, Config* config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL, save_to_disk, false, false,
false, false, 5, false, 0); false, false, 5, false, 0);
int exit_code = 0;
bool config_owned_by_pipeline = false;
int positional_args[2]; int positional_args[2];
int positional_count = 0; int positional_count = 0;
@@ -80,7 +82,7 @@ int main(int argc, char* argv[]) {
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0) { if (strcmp(argv[i], "--help") == 0) {
print_usage(); print_usage();
return 0; goto cleanup;
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) { } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
config->use_compression = true; config->use_compression = true;
config->use_multithreading = true; config->use_multithreading = true;
@@ -165,11 +167,13 @@ int main(int argc, char* argv[]) {
unsigned long long kbps = strtoull(argv[++i], &end, 10); unsigned long long kbps = strtoull(argv[++i], &end, 10);
if (errno != 0 || *end != '\0' || kbps == 0) { if (errno != 0 || *end != '\0' || kbps == 0) {
fprintf(stderr, "Error: --bwlimit must be a positive integer\n"); fprintf(stderr, "Error: --bwlimit must be a positive integer\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (kbps > ULLONG_MAX / 1024) { if (kbps > ULLONG_MAX / 1024) {
fprintf(stderr, "Error: --bwlimit value too large\n"); fprintf(stderr, "Error: --bwlimit value too large\n");
return 1; exit_code = 1;
goto cleanup;
} }
io_set_bwlimit(kbps * 1024); io_set_bwlimit(kbps * 1024);
log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps); log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps);
@@ -195,14 +199,16 @@ int main(int argc, char* argv[]) {
} else if (argv[i][0] == '-') { } else if (argv[i][0] == '-') {
fprintf(stderr, "Unknown option: %s\n", argv[i]); fprintf(stderr, "Unknown option: %s\n", argv[i]);
print_usage(); print_usage();
return 1; exit_code = 1;
goto cleanup;
} else { } else {
if (positional_count < 2) if (positional_count < 2)
positional_args[positional_count++] = i; positional_args[positional_count++] = i;
else { else {
fprintf(stderr, "Unexpected argument: %s\n", argv[i]); fprintf(stderr, "Unexpected argument: %s\n", argv[i]);
print_usage(); print_usage();
return 1; exit_code = 1;
goto cleanup;
} }
} }
} }
@@ -218,7 +224,8 @@ int main(int argc, char* argv[]) {
} else if (positional_count == 1) { } else if (positional_count == 1) {
fprintf(stderr, "Error: missing destination argument\n"); fprintf(stderr, "Error: missing destination argument\n");
print_usage(); print_usage();
return 1; exit_code = 1;
goto cleanup;
} else { } else {
if (!config->send_directory && env_source) if (!config->send_directory && env_source)
config->send_directory = str_dup((char*)env_source); config->send_directory = str_dup((char*)env_source);
@@ -229,22 +236,26 @@ int main(int argc, char* argv[]) {
if (!config->send_directory || !config->receive_root_directory) { if (!config->send_directory || !config->receive_root_directory) {
fprintf(stderr, "Error: source and destination directories are required\n"); fprintf(stderr, "Error: source and destination directories are required\n");
print_usage(); print_usage();
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) { 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 " fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk "
"serialization)\n"); "serialization)\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->transport == TRANSPORT_SSH && config->use_sendfile) { if (config->transport == TRANSPORT_SSH && config->use_sendfile) {
fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n"); fprintf(stderr, "Error: -f/--sendfile is not supported with SSH transport\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_incremental && config->use_chunk_serialization) { if (config->use_incremental && config->use_chunk_serialization) {
fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n"); fprintf(stderr, "Error: --incremental is not supported with -s (chunk serialization)\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_incremental && !config->use_metadata) { if (config->use_incremental && !config->use_metadata) {
@@ -254,15 +265,18 @@ int main(int argc, char* argv[]) {
if (config->use_delta && !config->use_incremental) { if (config->use_delta && !config->use_incremental) {
fprintf(stderr, "Error: --delta requires --incremental\n"); fprintf(stderr, "Error: --delta requires --incremental\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_delta && config->use_chunk_serialization) { if (config->use_delta && config->use_chunk_serialization) {
fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n"); fprintf(stderr, "Error: --delta cannot be combined with -s (chunk serialization)\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_delta && config->use_sendfile) { if (config->use_delta && config->use_sendfile) {
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n"); fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
return 1; exit_code = 1;
goto cleanup;
} }
if (config->use_delta && !config->use_metadata) { if (config->use_delta && !config->use_metadata) {
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta"); log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta");
@@ -272,12 +286,21 @@ int main(int argc, char* argv[]) {
if (config->use_tls) { if (config->use_tls) {
if (!config->tls_cert || !config->tls_key) { if (!config->tls_cert || !config->tls_key) {
fprintf(stderr, "Error: --tls requires --cert and --key\n"); fprintf(stderr, "Error: --tls requires --cert and --key\n");
return 1; exit_code = 1;
goto cleanup;
} }
tls_global_init(); tls_global_init();
} }
if (config->use_multithreading) if (config->use_multithreading) {
return send_files_multithreaded(config); config_owned_by_pipeline = true;
return send_files(config); exit_code = send_files_multithreaded(config);
} else {
exit_code = send_files(config);
}
cleanup:
if (!config_owned_by_pipeline)
config_delete(config);
return exit_code;
} }