Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a5bf67b59 | |||
| bb8bbeed5c | |||
| 2ffd324384 | |||
| d030ba655c |
@@ -1,10 +0,0 @@
|
||||
BasedOnStyle: LLVM
|
||||
IndentWidth: 2
|
||||
ColumnLimit: 100
|
||||
PointerAlignment: Left
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
SortIncludes: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
+41
-85
@@ -1,116 +1,72 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
on: [push, 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
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -S . -DSTRICT_WARNINGS=ON
|
||||
run: cmake -B build -S .
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Unit Tests
|
||||
run: ctest --test-dir build --output-on-failure -j$(nproc)
|
||||
run: ./build/tests
|
||||
|
||||
- name: Integration Tests
|
||||
run: python3 -m pytest tests/integration/ -v --tb=short
|
||||
run: python3 -m pytest tests/ -v --tb=short
|
||||
|
||||
sanitizers:
|
||||
sanitizer:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v9
|
||||
needs: lint
|
||||
strategy:
|
||||
matrix:
|
||||
sanitizer: [address, undefined]
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v6
|
||||
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: Configure with ASan + UBSan
|
||||
run: >
|
||||
cmake -B build -S .
|
||||
-DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g"
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build -j$(nproc)
|
||||
|
||||
- name: Unit Tests
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
run: ./build/tests || true
|
||||
|
||||
- name: Coverage Report
|
||||
- name: Integration Tests
|
||||
run: python3 -m pytest tests/ -v --tb=short || true
|
||||
|
||||
clang-tidy:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v6
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install clang-tidy
|
||||
run: apt-get update && apt-get install -y clang-tidy || true
|
||||
|
||||
- name: Configure (generate compile_commands.json)
|
||||
run: cmake -B build -S .
|
||||
|
||||
- name: Run clang-tidy
|
||||
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"
|
||||
if ! command -v clang-tidy &> /dev/null; then
|
||||
echo "clang-tidy not available, skipping"
|
||||
exit 0
|
||||
fi
|
||||
find src/ -name '*.c' | xargs clang-tidy -p build \
|
||||
--checks='-*,bugprone-*,clang-analyzer-*,misc-*,-misc-no-recursion' \
|
||||
2>&1 | tee clang-tidy-output.txt
|
||||
if grep -q " error:" clang-tidy-output.txt; then
|
||||
echo "clang-tidy found errors"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -2,6 +2,3 @@ build
|
||||
data_copied
|
||||
test_data/
|
||||
__pycache__/
|
||||
build-asan
|
||||
coverage.info
|
||||
build-*/
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# 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
|
||||
@@ -9,8 +9,6 @@ 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.
|
||||
|
||||
> **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
|
||||
@@ -112,24 +110,3 @@ When proposing architecture changes:
|
||||
- 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.
|
||||
|
||||
@@ -85,15 +85,3 @@ For each issue found, report:
|
||||
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.
|
||||
|
||||
@@ -21,29 +21,18 @@ 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)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(xxhash GIT_REPOSITORY https://github.com/Cyan4973/xxHash GIT_TAG v0.8.3 SOURCE_SUBDIR cmake_unofficial)
|
||||
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)
|
||||
|
||||
@@ -54,11 +43,13 @@ endif()
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
# Source file collection
|
||||
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")
|
||||
|
||||
# Targets
|
||||
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)
|
||||
@@ -84,7 +75,7 @@ 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)
|
||||
- **xxHash** — fetched via `FetchContent` from GitHub (delta transfer hashing)
|
||||
- **pthreads** — found via `find_package(Threads REQUIRED)`
|
||||
- **C11 standard** — required
|
||||
- **CMake 3.22+** — minimum version
|
||||
@@ -92,11 +83,10 @@ tests/integration/ — Python pytest integration tests
|
||||
## Conventions
|
||||
|
||||
- Use `file(GLOB ...)` for source collection (existing pattern).
|
||||
- All targets link `Threads::Threads`, `${ZSTD_LIBRARY}`, `OpenSSL::SSL`, `OpenSSL::Crypto`, and `xxhash`.
|
||||
- All targets link `Threads::Threads` and `${ZSTD_LIBRARY}`.
|
||||
- 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).
|
||||
- Sanitizer support is commented out but present (`-fsanitize=address`).
|
||||
- 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
|
||||
|
||||
@@ -105,21 +95,28 @@ tests/integration/ — Python pytest integration tests
|
||||
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).
|
||||
6. For sanitizer builds, use the commented-out `-fsanitize=address` lines as reference.
|
||||
7. Always verify the build compiles after changes.
|
||||
|
||||
## Sanitizer Configurations
|
||||
|
||||
Use the project's built-in `-DSANITIZER=` option (matching the CI matrix):
|
||||
### AddressSanitizer (memory errors)
|
||||
```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 -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
For UndefinedBehaviorSanitizer (no `-DSANITIZER=undefined` option in CMakeLists.txt yet), use the manual flag approach:
|
||||
### ThreadSanitizer (race conditions)
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=thread -g" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=thread"
|
||||
cmake --build build -j$(nproc)
|
||||
```
|
||||
|
||||
### UndefinedBehaviorSanitizer
|
||||
```bash
|
||||
cmake -B build -S . \
|
||||
-DCMAKE_C_FLAGS="-fsanitize=undefined -fno-omit-frame-pointer -g" \
|
||||
@@ -127,6 +124,14 @@ cmake -B build -S . \
|
||||
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)
|
||||
```bash
|
||||
cmake -B build -S . -DCMAKE_C_COMPILER_LAUNCHER=ccache
|
||||
@@ -164,31 +169,31 @@ cmake --build build -j$(nproc)
|
||||
./build/tests
|
||||
```
|
||||
|
||||
## Sanitizer Integration
|
||||
## When Adding Sanitizer Support to CMakeLists.txt
|
||||
|
||||
The project uses a single `SANITIZER` cache variable in `CMakeLists.txt`:
|
||||
Use CMake options for cleaner integration:
|
||||
```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`.
|
||||
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
|
||||
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
|
||||
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
|
||||
|
||||
Build with:
|
||||
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()
|
||||
```
|
||||
|
||||
Then build with:
|
||||
```bash
|
||||
cmake -B build -S . -DSANITIZER=address
|
||||
cmake --build build -j$(nproc)
|
||||
cmake -B build -S . -DENABLE_ASAN=ON
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -131,15 +131,3 @@ When explaining code:
|
||||
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.
|
||||
|
||||
@@ -1,323 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -155,15 +155,3 @@ For each bug found:
|
||||
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.
|
||||
|
||||
@@ -89,15 +89,3 @@ For each public function:
|
||||
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.
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -113,23 +113,20 @@ The project uses Gitea Actions. Key jobs:
|
||||
### Adding a New CI Job
|
||||
```yaml
|
||||
jobs:
|
||||
new-job:
|
||||
sanitizer:
|
||||
runs-on: ubuntu-latest
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v7
|
||||
container: gitea.tap-tap.win/taptap/fastsync-ci:v6
|
||||
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
|
||||
- name: Build with ASan + UBSan
|
||||
run: |
|
||||
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)
|
||||
- name: Run tests
|
||||
run: ./build/tests
|
||||
```
|
||||
The symlink step is required because `tests/conftest.py` expects `./build` to exist.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
@@ -149,15 +146,3 @@ When designing integration tests:
|
||||
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.
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -120,16 +120,4 @@ 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.
|
||||
```
|
||||
|
||||
@@ -84,15 +84,3 @@ When designing protocol changes:
|
||||
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.
|
||||
|
||||
@@ -153,15 +153,3 @@ Before and after each refactor, note:
|
||||
- **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.
|
||||
|
||||
@@ -139,15 +139,3 @@ 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.
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -173,7 +173,7 @@ When writing integration tests (Python-based), follow the patterns in `tests/int
|
||||
- `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`).
|
||||
Use `conftest.py` fixtures for server setup/teardown.
|
||||
|
||||
### Minimal Integration Test
|
||||
```python
|
||||
@@ -209,15 +209,3 @@ When asked to write tests, produce:
|
||||
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.
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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.
|
||||
|
||||
## Batch PR Workflow
|
||||
|
||||
When handling multiple issues split across several PRs that target the same files:
|
||||
|
||||
1. **Group issues by logical category** into separate PR branches (e.g., memory-safety, refactoring, test-coverage).
|
||||
2. **Fix and push** each branch independently. Let CI run on each PR.
|
||||
3. **Run all 3 reviewer types** on each PR and post results to Gitea via `tea pr approve/reject` or the Gitea API:
|
||||
- `reviewer` — general code correctness
|
||||
- `code-quality-guardian` — code quality, duplication, complexity
|
||||
- `security-auditor` — vulnerability assessment
|
||||
4. **Iterate**: if any reviewer requests changes, fix, push, re-review. Repeat until all 3 approve.
|
||||
5. **Merge approved PRs** one at a time into `main`.
|
||||
6. **Create a combined merge branch** for the remaining PRs that conflict with the new `main`:
|
||||
```bash
|
||||
git checkout -b merge-all origin/main
|
||||
for branch in branch1 branch2 branch3; do
|
||||
git merge origin/$branch --no-edit || true
|
||||
# Resolve conflicts, build, test
|
||||
done
|
||||
```
|
||||
7. **Run review again** on the combined branch. Fix issues, push, re-review until approved.
|
||||
8. **Merge** the combined PR, **close** the redundant individual PRs, and **close all resolved issues** via the Gitea API:
|
||||
```bash
|
||||
curl -s -X PATCH -H "Authorization: token $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"https://gitea.tap-tap.win/api/v1/repos/owner/repo/issues/<number>"
|
||||
```
|
||||
|
||||
## CI Troubleshooting
|
||||
|
||||
### If lint (clang-format) fails
|
||||
Run clang-format in the CI Docker image to match the exact CI version:
|
||||
```bash
|
||||
docker run --rm -v "$PWD:/workspace" -w /workspace gitea.tap-tap.win/taptap/fastsync-ci:v9 \
|
||||
sh -c 'find src/ tests/ -name "*.c" -o -name "*.h" | xargs clang-format -i'
|
||||
```
|
||||
|
||||
### If cppcheck fails
|
||||
Fix reported issues locally, then verify with:
|
||||
```bash
|
||||
docker run --rm -v "$PWD:/workspace" -w /workspace gitea.tap-tap.win/taptap/fastsync-ci:v9 \
|
||||
sh -c 'cppcheck --enable=warning,style,performance,portability --suppress=missingIncludeSystem --error-exitcode=1 --inline-suppr src/ tests/'
|
||||
```
|
||||
|
||||
### If integration tests fail
|
||||
Run locally before pushing:
|
||||
```bash
|
||||
python3 -m pytest tests/ -v --tb=short
|
||||
```
|
||||
|
||||
## Gitea API & tea CLI
|
||||
|
||||
### Check CI status via API
|
||||
```bash
|
||||
TOKEN="<token>"
|
||||
curl -s -H "Authorization: token $TOKEN" \
|
||||
"https://gitea.tap-tap.win/api/v1/repos/TapTap/FastSync/actions/runs?limit=5" \
|
||||
| python3 -c "
|
||||
import json,sys; d=json.load(sys.stdin)
|
||||
for r in d.get('workflow_runs',[]):
|
||||
path = r.get('path','')
|
||||
prn = path.split('@')[1].replace('refs/pull/','').replace('/head','') if '@' in path else ''
|
||||
print(f'PR #{prn}: sha={r[\"head_sha\"][:8]} {r[\"status\"]} {r.get(\"conclusion\",\"\")}')
|
||||
"
|
||||
```
|
||||
|
||||
### Post review comments
|
||||
```bash
|
||||
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
|
||||
-d '{"body":"MARKDOWN_REVIEW_BODY"}' \
|
||||
"https://gitea.tap-tap.win/api/v1/repos/TapTap/FastSync/issues/<PR_NUMBER>/comments"
|
||||
```
|
||||
|
||||
### Use tea for PR operations
|
||||
```bash
|
||||
tea pr list --repo TapTap/FastSync
|
||||
tea pr close <number> --repo TapTap/FastSync
|
||||
```
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`__thread` on shared SSL context**: io_ssl must NOT be thread-local — worker threads inherit the SSL context from the main thread. Use regular `static SSL* io_ssl`.
|
||||
- **SSL WANT_READ/WANT_WRITE retry**: Always retry on `SSL_ERROR_WANT_READ` and `SSL_ERROR_WANT_WRITE` in `send_n_data`/`receive_n_data`. Removing these breaks TLS multithreaded transfers.
|
||||
- **clang-format version**: The CI image uses clang-format 18. Always format inside the CI Docker container for exact match.
|
||||
- **Merge order matters**: Merge the most comprehensive branch first, then smaller ones, to minimize conflicts when creating a combined branch.
|
||||
+5
-58
@@ -7,36 +7,9 @@ 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)
|
||||
|
||||
# --- 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()
|
||||
# add_link_options(-fsanitize=address)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
@@ -60,8 +33,8 @@ 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} OpenSSL::SSL OpenSSL::Crypto xxhash)
|
||||
@@ -70,33 +43,7 @@ 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)
|
||||
|
||||
# --- 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)
|
||||
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)
|
||||
|
||||
# --- 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()
|
||||
|
||||
+2
-3
@@ -1,8 +1,7 @@
|
||||
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 && \
|
||||
gcc g++ make libc6-dev cmake libzstd-dev libssl-dev git ca-certificates curl \
|
||||
python3 python3-pip python3-venv openssl openssh-client && \
|
||||
pip3 install --break-system-packages pytest && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"instructions": ["AGENTS.md"],
|
||||
"permission": {
|
||||
"bash": {
|
||||
"*": "allow",
|
||||
"git push origin main": "deny",
|
||||
"git push main": "deny"
|
||||
"git push main": "ask"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-108
@@ -11,6 +11,9 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
char *server_host = "127.0.0.1";
|
||||
int server_port = 8080;
|
||||
|
||||
static void print_usage(void) {
|
||||
printf("Usage:\n");
|
||||
printf(" fastsync [options] <source> <destination>\n");
|
||||
@@ -35,10 +38,8 @@ static void print_usage(void) {
|
||||
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(" --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");
|
||||
@@ -55,31 +56,22 @@ static void print_usage(void) {
|
||||
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(" --fastsync-server-path <path>\n");
|
||||
printf(" Path to fastsync-server on remote (default: fastsync-server)\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");
|
||||
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)) {
|
||||
if (env_save &&
|
||||
(strcmp(env_save, "true") == 0 || strcmp(env_save, "1") == 0)) {
|
||||
save_to_disk = true;
|
||||
}
|
||||
|
||||
int exit_code = 0;
|
||||
bool config_owned_by_pipeline = false;
|
||||
Config* config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL, save_to_disk, false, false,
|
||||
false, false, 5, false, 0);
|
||||
if (config == NULL) {
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
}
|
||||
Config *config = config_create(str_dup(PROTOCOL_VERSION), NULL, NULL,
|
||||
save_to_disk, false, false, false, false, 5, false, 0);
|
||||
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
@@ -87,10 +79,7 @@ int main(int argc, char* argv[]) {
|
||||
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;
|
||||
return 0;
|
||||
} else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
|
||||
config->use_compression = true;
|
||||
config->use_multithreading = true;
|
||||
@@ -99,34 +88,17 @@ int main(int argc, char* argv[]) {
|
||||
} 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;
|
||||
config->ssh_port = atoi(argv[++i]);
|
||||
} 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]);
|
||||
int idx = config->exclude_count++;
|
||||
config->exclude_patterns = realloc(config->exclude_patterns, config->exclude_count * sizeof(char *));
|
||||
config->exclude_patterns[idx] = 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]);
|
||||
int idx = config->include_count++;
|
||||
config->include_patterns = realloc(config->include_patterns, config->include_count * sizeof(char *));
|
||||
config->include_patterns[idx] = 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) {
|
||||
@@ -151,11 +123,12 @@ int main(int argc, char* argv[]) {
|
||||
config->use_compression = true;
|
||||
log_message(LOG_LEVEL_INFO, "Enabled Compression");
|
||||
if (i + 1 < argc) {
|
||||
char* end_ptr;
|
||||
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);
|
||||
log_message(LOG_LEVEL_INFO, "Set Compression level to %d",
|
||||
config->compression_level);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -180,30 +153,21 @@ int main(int argc, char* argv[]) {
|
||||
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]);
|
||||
free(server_host);
|
||||
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;
|
||||
server_port = atoi(argv[++i]);
|
||||
} else if (strcmp(argv[i], "--bwlimit") == 0 && i + 1 < argc) {
|
||||
char* end;
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
if (kbps > ULLONG_MAX / 1024) {
|
||||
fprintf(stderr, "Error: --bwlimit value too large\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
return 1;
|
||||
}
|
||||
io_set_bwlimit(kbps * 1024);
|
||||
log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps);
|
||||
@@ -224,26 +188,19 @@ int main(int argc, char* argv[]) {
|
||||
} 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], "--fastsync-server-path") == 0 && i + 1 < argc) {
|
||||
free(config->fastsync_server_path);
|
||||
config->fastsync_server_path = str_dup(argv[++i]);
|
||||
} else if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) {
|
||||
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;
|
||||
return 1;
|
||||
} 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;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,38 +216,32 @@ int main(int argc, char* argv[]) {
|
||||
} else if (positional_count == 1) {
|
||||
fprintf(stderr, "Error: missing destination argument\n");
|
||||
print_usage();
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
return 1;
|
||||
} else {
|
||||
if (!config->send_directory && env_source)
|
||||
config->send_directory = str_dup((char*)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);
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
fprintf(stderr, "Error: -f/--sendfile cannot be combined with -c (compression) or -s (chunk serialization)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (config->use_incremental && !config->use_metadata) {
|
||||
@@ -300,18 +251,15 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
if (config->use_delta && !config->use_incremental) {
|
||||
fprintf(stderr, "Error: --delta requires --incremental\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
if (config->use_delta && config->use_sendfile) {
|
||||
fprintf(stderr, "Error: --delta cannot be combined with -f (sendfile)\n");
|
||||
exit_code = 1;
|
||||
goto cleanup;
|
||||
return 1;
|
||||
}
|
||||
if (config->use_delta && !config->use_metadata) {
|
||||
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta");
|
||||
@@ -321,21 +269,12 @@ int main(int argc, char* argv[]) {
|
||||
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;
|
||||
return 1;
|
||||
}
|
||||
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;
|
||||
if (config->use_multithreading)
|
||||
return send_files_multithreaded(config);
|
||||
return send_files(config);
|
||||
}
|
||||
|
||||
+160
-247
@@ -22,35 +22,28 @@
|
||||
#include <threads.h>
|
||||
#include <time.h>
|
||||
|
||||
static int incremental_check(Client* client, File* file, DeltaSignature** out_sig) {
|
||||
static int incremental_check(Client *client, File *file,
|
||||
DeltaSignature **out_sig) {
|
||||
*out_sig = NULL;
|
||||
if (!send_status(client->file_descriptor, STATUS_CHECK))
|
||||
return -1;
|
||||
if (!send_str(client->file_descriptor, file->path))
|
||||
return -1;
|
||||
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;
|
||||
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 (!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_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 *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;
|
||||
if (!sig) return -1;
|
||||
*out_sig = sig;
|
||||
return 2;
|
||||
}
|
||||
@@ -61,29 +54,27 @@ static int incremental_check(Client* client, File* file, DeltaSignature** out_si
|
||||
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;
|
||||
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;
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT)) return -1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Data* delta_data = delta_serialize(delta);
|
||||
Data *delta_data = delta_serialize(delta);
|
||||
delta_destroy(delta);
|
||||
if (!delta_data)
|
||||
return -1;
|
||||
if (!delta_data) return -1;
|
||||
|
||||
Data* to_send = delta_data;
|
||||
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;
|
||||
if (!to_send) return -1;
|
||||
}
|
||||
|
||||
bool ok = send_status(client->file_descriptor, STATUS_DELTA_DATA) &&
|
||||
@@ -96,156 +87,96 @@ static int send_delta(Client* client, File* file, DeltaSignature* sig, Config* c
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
typedef bool (*file_send_fn)(File*, int, bool, int, bool);
|
||||
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;
|
||||
static int send_file_incremental(Client *client, File *file, Config *config,
|
||||
file_send_fn send_fn) {
|
||||
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 < 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;
|
||||
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))
|
||||
if (!send_fn(file, client->file_descriptor, config->use_metadata,
|
||||
config->use_compression ? config->compression_level : 0, false))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_chunk(Client* client, Chunk* chunk, Config* config) {
|
||||
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 (!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;
|
||||
}
|
||||
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;
|
||||
} else if (config->use_sendfile && !config->use_compression) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (config->use_incremental) {
|
||||
int rc = send_file_incremental(client, chunk->items[i], config,
|
||||
(file_send_fn)file_send_sendfile);
|
||||
if (rc == 1) continue;
|
||||
if (rc < 0) return -1;
|
||||
} else {
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT)) return -1;
|
||||
if (!file_send_sendfile(chunk->items[i], client->file_descriptor, config->use_metadata, 0, true))
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (config->use_incremental) {
|
||||
int rc = send_file_incremental(client, chunk->items[i], config,
|
||||
(file_send_fn)file_send_single_calls);
|
||||
if (rc == 1) continue;
|
||||
if (rc < 0) return -1;
|
||||
} else {
|
||||
if (!send_status(client->file_descriptor, STATUS_NEXT)) return -1;
|
||||
if (!file_send_single_calls(chunk->items[i], client->file_descriptor,
|
||||
config->use_metadata,
|
||||
config->use_compression ? config->compression_level : 0,
|
||||
true))
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
Client* client;
|
||||
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,
|
||||
context->config->fastsync_server_path);
|
||||
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);
|
||||
if (!client || !client_connect_tls(client, server_host, 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);
|
||||
if (!client || !client_connect(client, server_host, server_port)) {
|
||||
if (client) client_delete(client);
|
||||
fprintf(stderr, "Error: could not connect to server\n");
|
||||
return thrd_error;
|
||||
}
|
||||
@@ -257,32 +188,24 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
}
|
||||
|
||||
while (true) {
|
||||
Chunk* current_chunk = queue_dequeue_multithreaded(
|
||||
context->queue_loader, &context->mutex_loader, &context->condition_not_empty_loader,
|
||||
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;
|
||||
}
|
||||
send_status(client->file_descriptor, STATUS_MANIFEST);
|
||||
send_int(client->file_descriptor, context->manifest->size);
|
||||
for (int i = 0; i < context->manifest->size; i++)
|
||||
send_str(client->file_descriptor,
|
||||
(char *)context->manifest->items[i]);
|
||||
}
|
||||
if (!send_status(client->file_descriptor, STATUS_FINISHED))
|
||||
goto send_fail;
|
||||
send_status(client->file_descriptor, STATUS_FINISHED);
|
||||
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");
|
||||
@@ -294,29 +217,30 @@ static int send_chunks_multithreaded(void* pipeline_context) {
|
||||
}
|
||||
}
|
||||
|
||||
static int scan_directory_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
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,
|
||||
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;
|
||||
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++;
|
||||
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,
|
||||
queue_enqueue_multithreaded(context->queue_scanner, current_chunk,
|
||||
&context->mutex_scanner,
|
||||
&context->condition_not_empty_scanner,
|
||||
&context->condition_not_full_scanner);
|
||||
}
|
||||
@@ -329,11 +253,12 @@ static int scan_directory_multithreaded(void* pipeline_context) {
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
static int load_files_multithreaded(void* pipeline_context) {
|
||||
PipelineContextSender* context = (PipelineContextSender*)pipeline_context;
|
||||
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,
|
||||
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);
|
||||
@@ -351,59 +276,60 @@ static int load_files_multithreaded(void* pipeline_context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
queue_enqueue_multithreaded(context->queue_loader, chunk, &context->mutex_loader,
|
||||
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) {
|
||||
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;
|
||||
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);
|
||||
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);
|
||||
printf("Total: %d files, %.1f MB\n", file_count,
|
||||
total_bytes / 1048576.0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Client* client;
|
||||
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, config->fastsync_server_path);
|
||||
if (!client)
|
||||
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);
|
||||
if (!client || !client_connect_tls(client, server_host, 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);
|
||||
if (!client || !client_connect(client, server_host, server_port)) {
|
||||
if (client) client_delete(client);
|
||||
fprintf(stderr, "Error: could not connect to server\n");
|
||||
return 1;
|
||||
}
|
||||
@@ -413,23 +339,23 @@ int send_files(Config* config) {
|
||||
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;
|
||||
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;
|
||||
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++;
|
||||
const char *p = current_chunk->items[i]->path;
|
||||
if (*p == '/') p++;
|
||||
array_list_add(manifest, str_dup(p));
|
||||
}
|
||||
}
|
||||
@@ -460,24 +386,13 @@ int send_files(Config* config) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
send_status(client->file_descriptor, STATUS_MANIFEST);
|
||||
send_int(client->file_descriptor, manifest->size);
|
||||
for (int i = 0; i < manifest->size; i++)
|
||||
send_str(client->file_descriptor, (char *)manifest->items[i]);
|
||||
array_list_delete(manifest);
|
||||
}
|
||||
if (!send_status(client->file_descriptor, STATUS_FINISHED))
|
||||
goto send_fail;
|
||||
send_status(client->file_descriptor, STATUS_FINISHED);
|
||||
Status s;
|
||||
int ok = receive_status(client->file_descriptor, &s) && s == STATUS_OK;
|
||||
if (config->show_progress) {
|
||||
@@ -489,47 +404,43 @@ int send_files(Config* config) {
|
||||
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) {
|
||||
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;
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
if (q1) queue_destroy(q1);
|
||||
if (q2) queue_destroy(q2);
|
||||
return 1;
|
||||
}
|
||||
PipelineContextSender* context = pipeline_context_sender_create(config, q1, q2);
|
||||
PipelineContextSender *context =
|
||||
pipeline_context_sender_create(config, q1, q2);
|
||||
if (!context) {
|
||||
queue_destroy(q1);
|
||||
queue_destroy(q2);
|
||||
@@ -539,9 +450,11 @@ int send_files_multithreaded(Config* config) {
|
||||
context->manifest = array_list_create(free);
|
||||
|
||||
thrd_t scanner, loader, sender;
|
||||
if (thrd_create(&scanner, scan_directory_multithreaded, context) != thrd_success ||
|
||||
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) {
|
||||
thrd_create(&sender, send_chunks_multithreaded, context) !=
|
||||
thrd_success) {
|
||||
perror("Error creating threads.\n");
|
||||
pipeline_context_sender_destroy(context);
|
||||
return 1;
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
#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);
|
||||
extern char *server_host;
|
||||
extern int server_port;
|
||||
|
||||
int send_chunk(Client *client, Chunk *chunk, Config *config);
|
||||
int send_files(Config *config);
|
||||
int send_files_multithreaded(Config *config);
|
||||
|
||||
#endif
|
||||
|
||||
+20
-178
@@ -11,103 +11,24 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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;
|
||||
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 *scanner = malloc(sizeof(DirectoryScanner));
|
||||
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_patterns = exclude_patterns;
|
||||
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_patterns = include_patterns;
|
||||
scanner->include_count = include_count;
|
||||
scanner->max_size = max_size;
|
||||
scanner->min_size = min_size;
|
||||
scanner->follow_symlinks = follow_symlinks;
|
||||
char* root_copy = str_dup(root_directory);
|
||||
if (root_copy == NULL) {
|
||||
for (int i = 0; i < scanner->include_count; i++)
|
||||
free(scanner->include_patterns[i]);
|
||||
free(scanner->include_patterns);
|
||||
for (int i = 0; i < scanner->exclude_count; i++)
|
||||
free(scanner->exclude_patterns[i]);
|
||||
free(scanner->exclude_patterns);
|
||||
queue_destroy(scanner->directories);
|
||||
free(scanner);
|
||||
return NULL;
|
||||
}
|
||||
queue_enqueue(scanner->directories, root_copy);
|
||||
queue_enqueue(scanner->directories, str_dup(root_directory));
|
||||
return scanner;
|
||||
}
|
||||
|
||||
void directory_scanner_destroy(DirectoryScanner* scanner) {
|
||||
void directory_scanner_destroy(DirectoryScanner *scanner) {
|
||||
if (scanner == NULL)
|
||||
return;
|
||||
if (scanner->current_dir) {
|
||||
@@ -115,27 +36,20 @@ void directory_scanner_destroy(DirectoryScanner* scanner) {
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
chunk_data->item_destroyer = NULL;
|
||||
array_list_delete(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) {
|
||||
static int open_next_directory(DirectoryScanner *scanner) {
|
||||
if (scanner->current_dir) {
|
||||
closedir(scanner->current_dir);
|
||||
scanner->current_dir = NULL;
|
||||
@@ -145,31 +59,28 @@ static int open_next_directory(DirectoryScanner* scanner) {
|
||||
if (queue_is_empty(scanner->directories))
|
||||
return 0;
|
||||
|
||||
scanner->current_path = (char*)queue_dequeue(scanner->directories);
|
||||
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 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
ArrayList* chunk_data = array_list_create(file_destroy);
|
||||
Chunk *directory_scanner_next(DirectoryScanner *scanner) {
|
||||
ArrayList *chunk_data = array_list_create(file_destroy);
|
||||
unsigned long long chunk_data_size = 0;
|
||||
|
||||
while (1) {
|
||||
if (scanner->current_dir == NULL) {
|
||||
int ret = open_next_directory(scanner);
|
||||
if (ret == 0)
|
||||
if (!open_next_directory(scanner))
|
||||
break;
|
||||
if (ret < 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
struct dirent* entry = readdir(scanner->current_dir);
|
||||
struct dirent *entry = readdir(scanner->current_dir);
|
||||
if (entry == NULL) {
|
||||
closedir(scanner->current_dir);
|
||||
scanner->current_dir = NULL;
|
||||
@@ -181,83 +92,15 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
|
||||
char* cur_path = path_cat(scanner->current_path, entry->d_name);
|
||||
char *cur_path = path_cat(scanner->current_path, entry->d_name);
|
||||
struct stat stats;
|
||||
// Use lstat to detect symlinks
|
||||
if (lstat(cur_path, &stats) != 0) {
|
||||
if (stat(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 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);
|
||||
if (file->link_target == NULL) {
|
||||
file_destroy(file);
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
file->data->size = 0;
|
||||
if (scanner->use_metadata)
|
||||
file->metadata = file_metadata_create(&stats);
|
||||
array_list_add(chunk_data, file);
|
||||
chunk_data_size += 1; // small size for symlinks
|
||||
if (chunk_data_size > scanner->chunk_size) {
|
||||
free(cur_path);
|
||||
return chunk_data_to_chunk(chunk_data);
|
||||
}
|
||||
free(cur_path);
|
||||
queue_enqueue(scanner->directories, (void *)cur_path);
|
||||
} else {
|
||||
bool excluded = false;
|
||||
for (int i = 0; i < scanner->exclude_count; i++) {
|
||||
@@ -291,7 +134,7 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
continue;
|
||||
}
|
||||
|
||||
File* file = file_create(cur_path);
|
||||
File *file = file_create(cur_path);
|
||||
if (file == NULL) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
@@ -311,6 +154,5 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
|
||||
if (chunk_data->size > 0)
|
||||
return chunk_data_to_chunk(chunk_data);
|
||||
array_list_delete(chunk_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+8
-19
@@ -7,32 +7,21 @@
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
Queue* directories;
|
||||
DIR* current_dir;
|
||||
char* current_path;
|
||||
Queue *directories;
|
||||
DIR *current_dir;
|
||||
char *current_path;
|
||||
bool use_metadata;
|
||||
unsigned long long chunk_size;
|
||||
char** exclude_patterns;
|
||||
char **exclude_patterns;
|
||||
int exclude_count;
|
||||
char** include_patterns;
|
||||
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, 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);
|
||||
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);
|
||||
Chunk *directory_scanner_next(DirectoryScanner *scanner);
|
||||
void directory_scanner_destroy(DirectoryScanner *scanner);
|
||||
|
||||
#endif
|
||||
|
||||
+25
-74
@@ -11,84 +11,43 @@
|
||||
#include "transport_tls.h"
|
||||
#include "unistd.h"
|
||||
#include "utils.h"
|
||||
#include <libgen.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// Check if a file path should be excluded based on config patterns
|
||||
static bool is_excluded(const char* path, const Config* config) {
|
||||
// Extract filename from path
|
||||
char* path_dup = str_dup(path);
|
||||
if (!path_dup)
|
||||
return false;
|
||||
/* basename(3) may return a pointer into path_dup or a static buffer;
|
||||
* either way we free path_dup, not fname. */
|
||||
char* fname = basename(path_dup);
|
||||
|
||||
// Check exclude patterns
|
||||
for (int i = 0; i < config->exclude_count; i++) {
|
||||
if (glob_match(config->exclude_patterns[i], fname)) {
|
||||
free(path_dup);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check include patterns (if any, file must match at least one)
|
||||
if (config->include_count > 0) {
|
||||
bool included = false;
|
||||
for (int i = 0; i < config->include_count; i++) {
|
||||
if (glob_match(config->include_patterns[i], fname)) {
|
||||
included = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!included) {
|
||||
free(path_dup);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
free(path_dup);
|
||||
return false;
|
||||
}
|
||||
|
||||
int receive_files(Config* config, int fd) {
|
||||
int receive_files(Config *config, int fd) {
|
||||
Status status;
|
||||
if (!receive_status(fd, &status))
|
||||
return -1;
|
||||
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 *file = receive_incremental_check(fd, config, &skipped);
|
||||
if (skipped) goto next;
|
||||
if (file == NULL && !skipped) return -1;
|
||||
if (config->save_to_disk)
|
||||
file_save_to_disk(config->receive_root_directory, file);
|
||||
file_destroy(file);
|
||||
} else if (status == STATUS_CHUNK) {
|
||||
Chunk* chunk = receive_chunk_data(fd, config);
|
||||
Chunk *chunk = receive_chunk_data(fd, config);
|
||||
if (chunk == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (config->save_to_disk && !is_excluded(chunk->items[i]->path, config))
|
||||
if (config->save_to_disk)
|
||||
file_save_to_disk(config->receive_root_directory, chunk->items[i]);
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
} else {
|
||||
File* file = file_receive(config, fd);
|
||||
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))
|
||||
if (config->save_to_disk)
|
||||
file_save_to_disk(config->receive_root_directory, file);
|
||||
file_destroy(file);
|
||||
}
|
||||
@@ -100,8 +59,7 @@ int receive_files(Config* config, int fd) {
|
||||
}
|
||||
|
||||
if (status == STATUS_MANIFEST) {
|
||||
if (receive_manifest(fd, config, &status) != 0)
|
||||
return -1;
|
||||
if (receive_manifest(fd, config, &status) != 0) return -1;
|
||||
}
|
||||
if (status != STATUS_FINISHED) {
|
||||
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
||||
@@ -113,20 +71,21 @@ int receive_files(Config* config, int fd) {
|
||||
}
|
||||
|
||||
void handler(int file_descriptor) {
|
||||
Config* config = config_receive(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) {
|
||||
Queue* q = queue_create(100, file_destroy);
|
||||
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);
|
||||
PipelineContextReceiver *context = pipeline_context_receiver_create(
|
||||
config, q, file_descriptor);
|
||||
if (context == NULL) {
|
||||
queue_destroy(q);
|
||||
config_delete(config);
|
||||
@@ -150,13 +109,14 @@ void handler(int file_descriptor) {
|
||||
close(file_descriptor);
|
||||
}
|
||||
|
||||
static Server* g_server = NULL;
|
||||
static volatile sig_atomic_t g_server_cleanup_requested = 0;
|
||||
static Server *g_server = NULL;
|
||||
|
||||
static void cleanup(int sig) {
|
||||
(void)sig;
|
||||
server_request_shutdown();
|
||||
g_server_cleanup_requested = 1;
|
||||
if (g_server) {
|
||||
server_delete(&g_server);
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
static void print_server_usage(void) {
|
||||
@@ -172,14 +132,13 @@ static void print_server_usage(void) {
|
||||
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[]) {
|
||||
int main(int argc, char *argv[]) {
|
||||
bool use_tls = false;
|
||||
char* tls_cert = NULL;
|
||||
char* tls_key = NULL;
|
||||
char* tls_ca = NULL;
|
||||
char *tls_cert = NULL;
|
||||
char *tls_key = NULL;
|
||||
char *tls_ca = NULL;
|
||||
int port = 8080;
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
@@ -187,9 +146,6 @@ int main(int argc, char* argv[]) {
|
||||
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);
|
||||
@@ -205,7 +161,7 @@ int main(int argc, char* argv[]) {
|
||||
} 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;
|
||||
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]);
|
||||
@@ -246,10 +202,5 @@ int main(int argc, char* argv[]) {
|
||||
} else {
|
||||
server_listen(g_server, handler);
|
||||
}
|
||||
|
||||
/* Graceful shutdown: delete the server */
|
||||
if (g_server_cleanup_requested)
|
||||
log_message(LOG_LEVEL_INFO, "Shutdown requested, cleaning up");
|
||||
server_delete(&g_server);
|
||||
return 0;
|
||||
}
|
||||
|
||||
+12
-14
@@ -3,14 +3,14 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
ArrayList* array_list_create(void (*item_destroyer)(void* item)) {
|
||||
ArrayList* list = (ArrayList*)malloc(sizeof(ArrayList));
|
||||
ArrayList *array_list_create(void (*item_destroyer)(void *item)) {
|
||||
ArrayList *list = (ArrayList *)malloc(sizeof(ArrayList));
|
||||
if (list == NULL) {
|
||||
perror("ERROR: Could not allocate memory for array list struct");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
list->items = malloc(INITIAL_ARRAY_SIZE * sizeof(void*));
|
||||
list->items = malloc(INITIAL_ARRAY_SIZE * sizeof(void *));
|
||||
if (list->items == NULL) {
|
||||
free(list);
|
||||
return NULL;
|
||||
@@ -21,7 +21,7 @@ ArrayList* array_list_create(void (*item_destroyer)(void* item)) {
|
||||
return list;
|
||||
}
|
||||
|
||||
void array_list_delete(ArrayList* array_list) {
|
||||
void array_list_delete(ArrayList *array_list) {
|
||||
if (array_list == NULL)
|
||||
return;
|
||||
if (array_list->item_destroyer != NULL) {
|
||||
@@ -34,13 +34,12 @@ void array_list_delete(ArrayList* array_list) {
|
||||
free(array_list);
|
||||
}
|
||||
|
||||
bool array_list_extend(ArrayList* array_list) {
|
||||
if (array_list == NULL)
|
||||
return false;
|
||||
bool array_list_extend(ArrayList *array_list) {
|
||||
if (array_list == NULL) return false;
|
||||
int new_capacity = array_list->capacity * 2;
|
||||
if (new_capacity == 0)
|
||||
new_capacity = INITIAL_ARRAY_SIZE;
|
||||
void* new_items = realloc(array_list->items, new_capacity * sizeof(void*));
|
||||
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;
|
||||
@@ -50,9 +49,8 @@ bool array_list_extend(ArrayList* array_list) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool array_list_add(ArrayList* array_list, void* item) {
|
||||
if (array_list == NULL)
|
||||
return false;
|
||||
bool array_list_add(ArrayList *array_list, void *item) {
|
||||
if (array_list == NULL) return false;
|
||||
if (array_list->capacity == array_list->size) {
|
||||
if (!array_list_extend(array_list))
|
||||
return false;
|
||||
@@ -62,15 +60,15 @@ bool array_list_add(ArrayList* array_list, void* item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void** array_list_to_array(const ArrayList* array_list) {
|
||||
void **array_list_to_array(ArrayList *array_list) {
|
||||
if (array_list == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
void** array = malloc(array_list->size * sizeof(void*));
|
||||
void **array = malloc(array_list->size * sizeof(void *));
|
||||
if (array == NULL) {
|
||||
perror("Could not malloc space for array from array list!");
|
||||
return NULL;
|
||||
}
|
||||
memcpy(array, array_list->items, array_list->size * sizeof(void*));
|
||||
memcpy(array, array_list->items, array_list->size * sizeof(void *));
|
||||
return array;
|
||||
}
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
#define INITIAL_ARRAY_SIZE 100
|
||||
|
||||
typedef struct ArrayList {
|
||||
void** items;
|
||||
void **items;
|
||||
int size;
|
||||
int capacity;
|
||||
void (*item_destroyer)(void* item);
|
||||
void (*item_destroyer)(void *item);
|
||||
} ArrayList;
|
||||
|
||||
ArrayList* array_list_create(void (*item_destroyer)(void* item));
|
||||
void array_list_delete(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);
|
||||
ArrayList *array_list_create(void (*item_destroyer)(void *item));
|
||||
void array_list_delete(ArrayList *array_list);
|
||||
bool array_list_extend(ArrayList *array_list);
|
||||
bool array_list_add(ArrayList *array_list, void *item);
|
||||
void **array_list_to_array(ArrayList *array_list);
|
||||
|
||||
#endif
|
||||
|
||||
+34
-47
@@ -12,14 +12,14 @@
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
|
||||
Chunk* chunk_create(File** items, int element_count) {
|
||||
Chunk* chunk = (Chunk*)malloc(sizeof(Chunk));
|
||||
Chunk *chunk_create(File **items, int element_count) {
|
||||
Chunk *chunk = (Chunk *)malloc(sizeof(Chunk));
|
||||
if (chunk == NULL) {
|
||||
perror("ERROR: Could not allocate memory for chunk structure");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
chunk->items = (File**)malloc(element_count * sizeof(File*));
|
||||
chunk->items = (File **)malloc(element_count * sizeof(File *));
|
||||
if (chunk->items == NULL) {
|
||||
free(chunk);
|
||||
return NULL;
|
||||
@@ -32,11 +32,11 @@ Chunk* chunk_create(File** items, int element_count) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
void chunk_destroy(void* item) {
|
||||
void chunk_destroy(void *item) {
|
||||
if (item == NULL) {
|
||||
return;
|
||||
}
|
||||
Chunk* chunk = (Chunk*)item;
|
||||
Chunk *chunk = (Chunk *)item;
|
||||
for (int i = 0; i < chunk->element_count; ++i) {
|
||||
if (chunk->items[i] != NULL) {
|
||||
file_destroy(chunk->items[i]);
|
||||
@@ -46,25 +46,26 @@ void chunk_destroy(void* item) {
|
||||
free(chunk);
|
||||
}
|
||||
|
||||
static unsigned long long per_file_serialize_size(File* file, bool use_metadata) {
|
||||
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_serialize(Chunk* chunk, bool use_metadata) {
|
||||
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 += per_file_serialize_size(chunk->items[i], use_metadata);
|
||||
}
|
||||
Data* data = data_create_empty(data_size);
|
||||
Data *data = data_create_empty(data_size);
|
||||
if (data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for chunk serialization");
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"Could not allocate memory for chunk serialization");
|
||||
return NULL;
|
||||
}
|
||||
char* data_pointer = data->data;
|
||||
char *data_pointer = data->data;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
File* file = chunk->items[i];
|
||||
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);
|
||||
@@ -83,9 +84,9 @@ Data* chunk_serialize(Chunk* chunk, bool use_metadata) {
|
||||
return data;
|
||||
}
|
||||
|
||||
Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
ArrayList* files = array_list_create(file_destroy);
|
||||
char* data_pointer = data->data;
|
||||
Chunk *chunk_deserialize(Data *data, bool use_metadata) {
|
||||
ArrayList *files = array_list_create(file_destroy);
|
||||
char *data_pointer = data->data;
|
||||
size_t remaining_size = data->size;
|
||||
|
||||
while (remaining_size > 0) {
|
||||
@@ -95,8 +96,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t path_len;
|
||||
memcpy(&path_len, data_pointer, sizeof(size_t));
|
||||
size_t path_len = *(size_t *)data_pointer;
|
||||
data_pointer += sizeof(size_t);
|
||||
remaining_size -= sizeof(size_t);
|
||||
|
||||
@@ -106,7 +106,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* path = malloc(path_len + 1);
|
||||
char *path = malloc(path_len + 1);
|
||||
if (path == NULL) {
|
||||
perror("Could not allocate memory for file path");
|
||||
array_list_delete(files);
|
||||
@@ -117,23 +117,10 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
data_pointer += path_len;
|
||||
remaining_size -= path_len;
|
||||
|
||||
File* file = file_create(path);
|
||||
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)
|
||||
@@ -146,8 +133,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
size_t file_data_size;
|
||||
memcpy(&file_data_size, data_pointer, sizeof(size_t));
|
||||
size_t file_data_size = *(size_t *)data_pointer;
|
||||
data_pointer += sizeof(size_t);
|
||||
remaining_size -= sizeof(size_t);
|
||||
|
||||
@@ -157,7 +143,7 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* file_data = malloc(file_data_size);
|
||||
void *file_data = malloc(file_data_size);
|
||||
if (file_data == NULL) {
|
||||
perror("Could not allocate memory for file data");
|
||||
array_list_delete(files);
|
||||
@@ -172,8 +158,8 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
array_list_add(files, file);
|
||||
}
|
||||
|
||||
File** file_array = (File**)array_list_to_array(files);
|
||||
Chunk* chunk = chunk_create(file_array, files->size);
|
||||
File **file_array = (File **)array_list_to_array(files);
|
||||
Chunk *chunk = chunk_create(file_array, files->size);
|
||||
|
||||
free(file_array);
|
||||
files->item_destroyer = NULL;
|
||||
@@ -182,26 +168,24 @@ Chunk* chunk_deserialize(Data* data, bool use_metadata) {
|
||||
return chunk;
|
||||
}
|
||||
|
||||
Data* chunk_compress(Chunk* chunk, int compression_level, bool use_metadata) {
|
||||
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 *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;
|
||||
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);
|
||||
Chunk *receive_chunk_data(int fd, 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;
|
||||
Data *data_to_process = chunk_data;
|
||||
if (config->use_compression) {
|
||||
data_to_process = data_decompress(chunk_data);
|
||||
data_destroy(chunk_data);
|
||||
@@ -210,9 +194,12 @@ Chunk* receive_chunk_data(int fd, const Config* config) {
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
Chunk* chunk = chunk_deserialize(data_to_process, config->use_metadata);
|
||||
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
-7
@@ -10,15 +10,15 @@
|
||||
#define DESIRED_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
|
||||
typedef struct {
|
||||
File** items;
|
||||
File **items;
|
||||
int element_count;
|
||||
} Chunk;
|
||||
|
||||
Chunk* chunk_create(File** items, int element_count);
|
||||
void chunk_destroy(void* chunk);
|
||||
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);
|
||||
Chunk *chunk_create(File **items, int element_count);
|
||||
void chunk_destroy(void *chunk);
|
||||
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, Config *config);
|
||||
|
||||
#endif
|
||||
|
||||
+20
-46
@@ -1,46 +1,24 @@
|
||||
#include "compression.h"
|
||||
#include "data.h"
|
||||
#include "log.h"
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include "stdlib.h"
|
||||
#include "zstd.h"
|
||||
|
||||
#define INITIAL_DECOMPRESS_BUF_SIZE (1024 * 1024)
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
Data *data_compress(Data *data_to_compress, int compression_level) {
|
||||
log_message(LOG_LEVEL_DEBUG, "Starting to compress data");
|
||||
|
||||
/* 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;
|
||||
Data *compressed_data = data_create_empty(dst_size);
|
||||
if (compressed_data == NULL) return NULL;
|
||||
|
||||
ZSTD_CCtx* cctx = ZSTD_createCCtx();
|
||||
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};
|
||||
|
||||
@@ -48,7 +26,8 @@ Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
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));
|
||||
log_message(LOG_LEVEL_ERROR, "Compression failed: %s",
|
||||
ZSTD_getErrorName(ret));
|
||||
ZSTD_freeCCtx(cctx);
|
||||
data_destroy(compressed_data);
|
||||
return NULL;
|
||||
@@ -63,33 +42,27 @@ Data* data_compress(Data* data_to_compress, int compression_level) {
|
||||
return compressed_data;
|
||||
}
|
||||
|
||||
Data* data_decompress(Data* 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);
|
||||
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();
|
||||
ZSTD_DCtx *dctx = ZSTD_createDCtx();
|
||||
if (!dctx) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to create ZSTD decompression context");
|
||||
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);
|
||||
size_t buf_size = (!ZSTD_isError(dst_size) && dst_size > 0)
|
||||
? (size_t)dst_size
|
||||
: INITIAL_DECOMPRESS_BUF_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);
|
||||
@@ -103,14 +76,15 @@ Data* data_decompress(Data* compressed_data) {
|
||||
do {
|
||||
ret = ZSTD_decompressStream(dctx, &output, &input);
|
||||
if (ZSTD_isError(ret)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Decompression failed: %s", ZSTD_getErrorName(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);
|
||||
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);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "data.h"
|
||||
|
||||
Data* data_compress(Data* data_to_compress, int compression_level);
|
||||
Data* data_decompress(Data* compressed_data);
|
||||
Data *data_compress(Data *data_to_compress, int compression_level);
|
||||
Data *data_decompress(Data *compressed_data);
|
||||
|
||||
#endif
|
||||
|
||||
+60
-229
@@ -4,19 +4,18 @@
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.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, bool use_metadata, int compression_level,
|
||||
bool use_sendfile, unsigned long long chunk_size) {
|
||||
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));
|
||||
if (config == NULL)
|
||||
return NULL;
|
||||
Config *config = malloc(sizeof(Config));
|
||||
config->version = version;
|
||||
config->send_directory = send_directory;
|
||||
config->receive_root_directory = receive_directory;
|
||||
@@ -34,7 +33,6 @@ Config* config_create(char* version, char* send_directory, char* receive_directo
|
||||
config->ssh_port = 22;
|
||||
config->transport = TRANSPORT_TCP;
|
||||
config->ssh_destination = NULL;
|
||||
config->fastsync_server_path = NULL;
|
||||
config->exclude_patterns = NULL;
|
||||
config->exclude_count = 0;
|
||||
config->include_patterns = NULL;
|
||||
@@ -49,47 +47,35 @@ Config* config_create(char* version, char* send_directory, char* receive_directo
|
||||
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;
|
||||
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;
|
||||
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);
|
||||
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) {
|
||||
if (config == NULL)
|
||||
return;
|
||||
void config_delete(Config *config) {
|
||||
free(config->version);
|
||||
free(config->send_directory);
|
||||
free(config->receive_root_directory);
|
||||
free(config->ssh_destination);
|
||||
free(config->fastsync_server_path);
|
||||
for (int i = 0; i < config->exclude_count; i++)
|
||||
free(config->exclude_patterns[i]);
|
||||
free(config->exclude_patterns);
|
||||
@@ -99,66 +85,28 @@ void config_delete(Config* config) {
|
||||
free(config->tls_cert);
|
||||
free(config->tls_key);
|
||||
free(config->tls_ca);
|
||||
free(config->server_host);
|
||||
free(config);
|
||||
}
|
||||
|
||||
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;
|
||||
bool config_send(int file_descriptor, 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_int(file_descriptor, (int)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;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return false;
|
||||
if (!receive_status(file_descriptor, &status)) return false;
|
||||
if (status != STATUS_OK) {
|
||||
log_message(LOG_LEVEL_ERROR, "Error transmitting config");
|
||||
return false;
|
||||
@@ -166,81 +114,54 @@ bool config_send(int file_descriptor, const Config* config) {
|
||||
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 *config_receive(int file_descriptor) {
|
||||
Config *config = (Config *)malloc(sizeof(Config));
|
||||
if (config == NULL) return NULL;
|
||||
config->version = receive_str(file_descriptor);
|
||||
if (!config->version) {
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->save_to_disk = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_multithreading = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_chunk_serialization = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_compression = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_metadata = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
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;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->chunk_size = (unsigned long long)tmp;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_sendfile = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_delete = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_incremental = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
if (!receive_int(file_descriptor, &tmp)) goto error;
|
||||
config->use_delta = tmp;
|
||||
if (!receive_int(file_descriptor, &tmp))
|
||||
goto error;
|
||||
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;
|
||||
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->fastsync_server_path = NULL;
|
||||
config->exclude_patterns = NULL;
|
||||
config->exclude_count = 0;
|
||||
config->include_patterns = NULL;
|
||||
@@ -251,103 +172,13 @@ Config* config_receive(int file_descriptor) {
|
||||
config->tls_cert = NULL;
|
||||
config->tls_key = NULL;
|
||||
config->tls_ca = NULL;
|
||||
config->follow_symlinks = false;
|
||||
config->partial = false;
|
||||
|
||||
#define MAX_PATTERN_COUNT 10000
|
||||
|
||||
// Receive exclude patterns
|
||||
int ec;
|
||||
if (!receive_int(file_descriptor, &ec))
|
||||
goto error;
|
||||
if (ec > MAX_PATTERN_COUNT) {
|
||||
log_message(LOG_LEVEL_ERROR, "Exclude pattern count %d exceeds maximum %d", ec,
|
||||
MAX_PATTERN_COUNT);
|
||||
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;
|
||||
if (ic > MAX_PATTERN_COUNT) {
|
||||
log_message(LOG_LEVEL_ERROR, "Include pattern count %d exceeds maximum %d", ic,
|
||||
MAX_PATTERN_COUNT);
|
||||
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;
|
||||
if (!send_status(file_descriptor, STATUS_OK)) goto error;
|
||||
return config;
|
||||
|
||||
error:
|
||||
free(config->version);
|
||||
free(config->send_directory);
|
||||
free(config->receive_root_directory);
|
||||
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->ssh_destination);
|
||||
free(config->server_host);
|
||||
free(config);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
+25
-35
@@ -3,14 +3,16 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
typedef enum { TRANSPORT_TCP, TRANSPORT_SSH } TransportType;
|
||||
typedef enum {
|
||||
TRANSPORT_TCP,
|
||||
TRANSPORT_SSH
|
||||
} TransportType;
|
||||
|
||||
typedef struct Config {
|
||||
char* version;
|
||||
char* send_directory;
|
||||
char* receive_root_directory;
|
||||
char *version;
|
||||
char *send_directory;
|
||||
char *receive_root_directory;
|
||||
bool save_to_disk;
|
||||
bool use_multithreading;
|
||||
bool use_chunk_serialization;
|
||||
@@ -24,11 +26,10 @@ typedef struct Config {
|
||||
unsigned long long chunk_size;
|
||||
int ssh_port;
|
||||
TransportType transport;
|
||||
char* ssh_destination;
|
||||
char* fastsync_server_path;
|
||||
char** exclude_patterns;
|
||||
char *ssh_destination;
|
||||
char **exclude_patterns;
|
||||
int exclude_count;
|
||||
char** include_patterns;
|
||||
char **include_patterns;
|
||||
int include_count;
|
||||
unsigned long long max_size;
|
||||
unsigned long long min_size;
|
||||
@@ -37,35 +38,24 @@ typedef struct Config {
|
||||
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;
|
||||
int timeout;
|
||||
int contimeout;
|
||||
bool quiet;
|
||||
bool backup;
|
||||
char* backup_dir;
|
||||
bool stats;
|
||||
int max_depth;
|
||||
FILE* log_file;
|
||||
int queue_size;
|
||||
bool follow_symlinks;
|
||||
bool partial;
|
||||
char *tls_cert;
|
||||
char *tls_key;
|
||||
char *tls_ca;
|
||||
} Config;
|
||||
|
||||
#define PROTOCOL_VERSION "2.0.0"
|
||||
#define PROTOCOL_VERSION "1.2.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);
|
||||
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);
|
||||
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);
|
||||
bool config_send(int file_descriptor, Config *config);
|
||||
Config *config_receive(int file_descriptor);
|
||||
bool is_remote_dest(const char *s);
|
||||
void config_parse_ssh_dest(Config *config);
|
||||
|
||||
#endif
|
||||
|
||||
+9
-12
@@ -1,11 +1,9 @@
|
||||
#include "data.h"
|
||||
#include "log.h"
|
||||
#include <stdlib.h>
|
||||
#include "stdlib.h"
|
||||
|
||||
Data* data_create_empty(size_t 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);
|
||||
Data *data_create_empty(size_t data_size) {
|
||||
void *data = malloc(data_size);
|
||||
if (data == NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Could not allocate memory for empty data");
|
||||
return NULL;
|
||||
@@ -13,8 +11,8 @@ Data* data_create_empty(size_t data_size) {
|
||||
return data_create(data, data_size);
|
||||
}
|
||||
|
||||
Data* data_create_reserve(size_t size) {
|
||||
Data* d = malloc(sizeof(Data));
|
||||
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;
|
||||
@@ -24,8 +22,8 @@ Data* data_create_reserve(size_t size) {
|
||||
return d;
|
||||
}
|
||||
|
||||
Data* data_create(void* data, size_t data_size) {
|
||||
Data* new_data = malloc(sizeof(Data));
|
||||
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");
|
||||
free(data);
|
||||
@@ -36,9 +34,8 @@ Data* data_create(void* data, size_t data_size) {
|
||||
return new_data;
|
||||
}
|
||||
|
||||
void data_destroy(Data* data) {
|
||||
if (data == NULL)
|
||||
return;
|
||||
void data_destroy(Data *data) {
|
||||
if (data == NULL) return;
|
||||
free(data->data);
|
||||
free(data);
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,16 +1,16 @@
|
||||
#ifndef DATA_H
|
||||
#define DATA_H
|
||||
|
||||
#include <stdlib.h>
|
||||
#include "stdlib.h"
|
||||
|
||||
typedef struct {
|
||||
void* data;
|
||||
void *data;
|
||||
size_t size;
|
||||
} 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_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);
|
||||
|
||||
#endif
|
||||
|
||||
+74
-91
@@ -7,8 +7,8 @@
|
||||
#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 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++) {
|
||||
@@ -18,20 +18,20 @@ uint32_t delta_adler32(const void* data, uint32_t len) {
|
||||
return (s2 << 16) | s1;
|
||||
}
|
||||
|
||||
uint32_t delta_xxhash32(const void* data, uint32_t len) {
|
||||
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) {
|
||||
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;
|
||||
DeltaSignature *sig = malloc(sizeof(DeltaSignature));
|
||||
if (!sig) return NULL;
|
||||
|
||||
sig->file_size = old_file_size;
|
||||
sig->block_size = block_size;
|
||||
@@ -42,11 +42,12 @@ DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_f
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const uint8_t* data = (const uint8_t*)old_file_data;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -54,16 +55,14 @@ DeltaSignature* delta_signature_create(const void* old_file_data, uint64_t old_f
|
||||
return sig;
|
||||
}
|
||||
|
||||
Data* delta_signature_serialize(const DeltaSignature* sig) {
|
||||
if (!sig)
|
||||
return NULL;
|
||||
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;
|
||||
uint8_t *buf = malloc((size_t)total);
|
||||
if (!buf) return NULL;
|
||||
|
||||
size_t pos = 0;
|
||||
memcpy(buf + pos, &sig->file_size, sizeof(uint64_t));
|
||||
@@ -83,16 +82,15 @@ Data* delta_signature_serialize(const DeltaSignature* sig) {
|
||||
return data_create(buf, (size_t)total);
|
||||
}
|
||||
|
||||
DeltaSignature* delta_signature_deserialize(const Data* data) {
|
||||
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;
|
||||
const uint8_t *buf = (const uint8_t *)data->data;
|
||||
size_t pos = 0;
|
||||
|
||||
DeltaSignature* sig = malloc(sizeof(DeltaSignature));
|
||||
if (!sig)
|
||||
return NULL;
|
||||
DeltaSignature *sig = malloc(sizeof(DeltaSignature));
|
||||
if (!sig) return NULL;
|
||||
|
||||
memcpy(&sig->file_size, buf + pos, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
@@ -124,35 +122,31 @@ DeltaSignature* delta_signature_deserialize(const Data* data) {
|
||||
return sig;
|
||||
}
|
||||
|
||||
void delta_signature_destroy(DeltaSignature* sig) {
|
||||
if (!sig)
|
||||
return;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
@@ -161,18 +155,17 @@ static bool flush_literal(DeltaInstruction** instrs, uint32_t* capacity, uint32_
|
||||
return true;
|
||||
}
|
||||
|
||||
Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const DeltaSignature* sig,
|
||||
uint32_t block_size) {
|
||||
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;
|
||||
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;
|
||||
DeltaInstruction *instrs = malloc(capacity * sizeof(DeltaInstruction));
|
||||
if (!instrs) return NULL;
|
||||
|
||||
uint64_t literal_start = 0;
|
||||
bool has_literal = false;
|
||||
@@ -183,17 +176,20 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
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);
|
||||
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;
|
||||
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) %
|
||||
(uint32_t)((uint64_t)block_size * old_byte % DELTA_ADLER32_MODULUS) +
|
||||
s1 - 1) %
|
||||
DELTA_ADLER32_MODULUS;
|
||||
adler = (s2 << 16) | s1;
|
||||
} else {
|
||||
@@ -213,7 +209,8 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
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)) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data,
|
||||
literal_start, i)) {
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
@@ -248,13 +245,14 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
}
|
||||
|
||||
if (has_literal) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data, literal_start, new_file_size)) {
|
||||
if (!flush_literal(&instrs, &capacity, &count, new_data,
|
||||
literal_start, new_file_size)) {
|
||||
free(instrs);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
Delta* delta = malloc(sizeof(Delta));
|
||||
Delta *delta = malloc(sizeof(Delta));
|
||||
if (!delta) {
|
||||
for (uint32_t k = 0; k < count; k++) {
|
||||
if (instrs[k].type == DELTA_INSTR_LITERAL)
|
||||
@@ -281,14 +279,12 @@ Delta* delta_compute(const void* new_file_data, uint64_t new_file_size, const De
|
||||
return delta;
|
||||
}
|
||||
|
||||
Data* delta_serialize(const Delta* delta) {
|
||||
if (!delta)
|
||||
return NULL;
|
||||
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;
|
||||
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));
|
||||
@@ -311,7 +307,8 @@ Data* delta_serialize(const Delta* delta) {
|
||||
} 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);
|
||||
memcpy(buf + pos, delta->instructions[i].literal.data,
|
||||
delta->instructions[i].literal.length);
|
||||
pos += delta->instructions[i].literal.length;
|
||||
}
|
||||
}
|
||||
@@ -319,16 +316,15 @@ Data* delta_serialize(const Delta* delta) {
|
||||
return data_create(buf, (size_t)total);
|
||||
}
|
||||
|
||||
Delta* delta_deserialize(const Data* data) {
|
||||
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;
|
||||
const uint8_t *buf = (const uint8_t *)data->data;
|
||||
size_t pos = 0;
|
||||
|
||||
Delta* delta = malloc(sizeof(Delta));
|
||||
if (!delta)
|
||||
return NULL;
|
||||
Delta *delta = malloc(sizeof(Delta));
|
||||
if (!delta) return NULL;
|
||||
|
||||
memcpy(&delta->new_file_size, buf + pos, sizeof(uint64_t));
|
||||
pos += sizeof(uint64_t);
|
||||
@@ -376,24 +372,15 @@ Delta* delta_deserialize(const Data* data) {
|
||||
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;
|
||||
@@ -421,22 +408,21 @@ Delta* delta_deserialize(const Data* data) {
|
||||
return delta;
|
||||
}
|
||||
|
||||
void* delta_apply(const void* old_data, uint64_t old_size, const Delta* 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;
|
||||
if (!old_data || !delta) return NULL;
|
||||
|
||||
void* output = malloc((size_t)delta->new_file_size);
|
||||
if (!output)
|
||||
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;
|
||||
uint8_t *out = (uint8_t *)output;
|
||||
uint8_t *old = (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;
|
||||
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;
|
||||
|
||||
@@ -461,9 +447,8 @@ void* delta_apply(const void* old_data, uint64_t old_size, const Delta* delta,
|
||||
return output;
|
||||
}
|
||||
|
||||
void delta_destroy(Delta* delta) {
|
||||
if (!delta)
|
||||
return;
|
||||
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);
|
||||
@@ -484,9 +469,8 @@ bool delta_should_attempt(uint64_t old_size, uint64_t new_size, uint64_t max_fil
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size) {
|
||||
if (!delta || delta->instruction_count == 0)
|
||||
return false;
|
||||
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++) {
|
||||
@@ -495,8 +479,7 @@ bool delta_is_worthwhile(const Delta* delta, uint64_t new_file_size) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_match)
|
||||
return false;
|
||||
if (!has_match) return false;
|
||||
|
||||
double ratio = (double)delta->delta_size / (double)new_file_size;
|
||||
return ratio < DELTA_FALLBACK_RATIO;
|
||||
|
||||
+33
-28
@@ -6,17 +6,17 @@
|
||||
#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_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
|
||||
#define DELTA_OP_BLOCK_MATCH 0x01
|
||||
#define DELTA_OP_LITERAL 0x02
|
||||
|
||||
typedef struct {
|
||||
uint32_t adler32;
|
||||
@@ -27,10 +27,13 @@ typedef struct {
|
||||
uint64_t file_size;
|
||||
uint32_t block_size;
|
||||
uint32_t block_count;
|
||||
DeltaBlockSig* blocks;
|
||||
DeltaBlockSig *blocks;
|
||||
} DeltaSignature;
|
||||
|
||||
typedef enum { DELTA_INSTR_BLOCK_MATCH = 0x01, DELTA_INSTR_LITERAL = 0x02 } DeltaInstrType;
|
||||
typedef enum {
|
||||
DELTA_INSTR_BLOCK_MATCH = 0x01,
|
||||
DELTA_INSTR_LITERAL = 0x02
|
||||
} DeltaInstrType;
|
||||
|
||||
typedef struct {
|
||||
DeltaInstrType type;
|
||||
@@ -41,7 +44,7 @@ typedef struct {
|
||||
uint32_t length;
|
||||
} match;
|
||||
struct {
|
||||
uint8_t* data;
|
||||
uint8_t *data;
|
||||
uint32_t length;
|
||||
} literal;
|
||||
};
|
||||
@@ -50,27 +53,29 @@ typedef struct {
|
||||
typedef struct {
|
||||
uint64_t new_file_size;
|
||||
uint32_t instruction_count;
|
||||
DeltaInstruction* instructions;
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
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);
|
||||
uint32_t delta_adler32(const void *data, uint32_t len);
|
||||
uint32_t delta_xxhash32(const void *data, uint32_t len);
|
||||
|
||||
#endif
|
||||
|
||||
+128
-390
@@ -1,9 +1,7 @@
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libgen.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -22,24 +20,21 @@
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define STREAM_THRESHOLD (64ULL * 1024 * 1024) /* 64 MB */
|
||||
#define STREAM_CHUNK_SIZE (1ULL * 1024 * 1024) /* 1 MB */
|
||||
|
||||
File* file_create(const char* path) {
|
||||
File* file = (File*)malloc(sizeof(File));
|
||||
File *file_create(const char *path) {
|
||||
File *file = (File *)malloc(sizeof(File));
|
||||
if (file == NULL) {
|
||||
perror("ERROR: Could not allocate memory for file struct");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int path_len = strlen(path);
|
||||
file->path = (char*)malloc(path_len + 1);
|
||||
file->path = (char *)malloc(path_len + 1);
|
||||
if (file->path == NULL) {
|
||||
free(file);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
memcpy(file->path, path, path_len + 1);
|
||||
strcpy(file->path, path);
|
||||
file->data = data_create_reserve(0);
|
||||
if (file->data == NULL) {
|
||||
free(file->path);
|
||||
@@ -47,28 +42,24 @@ File* file_create(const char* path) {
|
||||
return NULL;
|
||||
}
|
||||
file->metadata = NULL;
|
||||
file->type = FILE_TYPE_REGULAR;
|
||||
file->link_target = NULL;
|
||||
return file;
|
||||
}
|
||||
|
||||
void file_destroy(void* item) {
|
||||
void file_destroy(void *item) {
|
||||
if (item == NULL)
|
||||
return;
|
||||
File* file = (File*)item;
|
||||
File *file = (File *)item;
|
||||
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);
|
||||
}
|
||||
|
||||
FileMetadata* file_metadata_create(const struct stat* stats) {
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
FileMetadata *file_metadata_create(struct stat *stats) {
|
||||
FileMetadata *m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL) {
|
||||
perror("ERROR: Could not allocate memory for file metadata");
|
||||
return NULL;
|
||||
@@ -85,21 +76,12 @@ FileMetadata* file_metadata_create(const struct stat* stats) {
|
||||
return m;
|
||||
}
|
||||
|
||||
void file_metadata_destroy(void* metadata) {
|
||||
void file_metadata_destroy(void *metadata) {
|
||||
free(metadata);
|
||||
}
|
||||
|
||||
bool file_load_data(File* file) {
|
||||
if (file == NULL)
|
||||
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;
|
||||
}
|
||||
bool file_load_data(File *file) {
|
||||
if (file == NULL) return false;
|
||||
if (file->data->data == NULL) {
|
||||
file->data->data = malloc(file->data->size);
|
||||
if (file->data->data == NULL) {
|
||||
@@ -110,86 +92,14 @@ bool file_load_data(File* file) {
|
||||
size_t bytes_read = file_content_to_buffer(file);
|
||||
if (bytes_read != file->data->size) {
|
||||
log_message(LOG_LEVEL_ERROR, "Did not read expected amount of bytes from file");
|
||||
free(file->data->data);
|
||||
file->data->data = NULL;
|
||||
file->data->size = 0;
|
||||
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 = malloc(STREAM_CHUNK_SIZE);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
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");
|
||||
}
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
free(buf);
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
if (!send_n_data(file_descriptor, buf, nread)) {
|
||||
send_status(file_descriptor, STATUS_ERROR);
|
||||
free(buf);
|
||||
fclose(fp);
|
||||
return false;
|
||||
}
|
||||
remaining -= (unsigned long long)nread;
|
||||
}
|
||||
free(buf);
|
||||
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);
|
||||
}
|
||||
|
||||
bool file_send_single_calls(File *file, int file_descriptor, bool use_metadata, int compression_level, bool send_path) {
|
||||
Data *data_to_send = file->data;
|
||||
Data *compressed_data = NULL;
|
||||
if (compression_level > 0) {
|
||||
compressed_data = data_compress(file->data, compression_level);
|
||||
if (compressed_data == NULL) {
|
||||
@@ -198,6 +108,14 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||
}
|
||||
data_to_send = compressed_data;
|
||||
}
|
||||
if (send_path && !send_str(file_descriptor, file->path)) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
}
|
||||
if (use_metadata && !metadata_send(file_descriptor, file->metadata)) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
}
|
||||
if (!send_data(file_descriptor, data_to_send)) {
|
||||
data_destroy(compressed_data);
|
||||
return false;
|
||||
@@ -206,130 +124,42 @@ bool file_send_single_calls(File* file, int file_descriptor, bool use_metadata,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool file_save_to_disk(const char* root_directory, File* file) {
|
||||
if (file->type == FILE_TYPE_SYMLINK && file->link_target) {
|
||||
// Validate link_target — reject absolute paths or traversal
|
||||
if (file->link_target[0] == '/' || strstr(file->link_target, "..") != NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Path traversal blocked in symlink target: %s",
|
||||
file->link_target);
|
||||
return false;
|
||||
}
|
||||
char* disk_path = path_cat((char*)root_directory, file->path);
|
||||
if (disk_path == NULL)
|
||||
return false;
|
||||
if (strstr(disk_path, "..") != NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Path traversal blocked: %s", disk_path);
|
||||
free(disk_path);
|
||||
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;
|
||||
if (strstr(disk_path, "..") != NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Path traversal blocked: %s", disk_path);
|
||||
free(disk_path);
|
||||
return false;
|
||||
}
|
||||
bool file_save_to_disk(const char *root_directory, File *file) {
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
static File *receive_delta_file(int fd, Config *config, const char *check_path,
|
||||
void *old_data, unsigned long long old_size) {
|
||||
if (!old_data) return NULL;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
DeltaSignature *sig = delta_signature_create(old_data, old_size,
|
||||
config->delta_block_size);
|
||||
if (!sig) { free(old_data); return NULL; }
|
||||
|
||||
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) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
Data *sig_data = delta_signature_serialize(sig);
|
||||
if (!sig_data) { delta_signature_destroy(sig); free(old_data); return NULL; }
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, old_size, config->delta_block_size);
|
||||
if (!sig) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* sig_data = delta_signature_serialize(sig);
|
||||
if (!sig_data) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool sig_sent = send_status(fd, STATUS_DELTA_SIGNATURE) && send_data(fd, sig_data);
|
||||
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;
|
||||
}
|
||||
if (!sig_sent) { delta_signature_destroy(sig); free(old_data); return NULL; }
|
||||
|
||||
Status resp;
|
||||
if (!receive_status(fd, &resp)) {
|
||||
@@ -339,7 +169,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
}
|
||||
|
||||
if (resp == STATUS_DELTA_DATA) {
|
||||
Data* delta_data = receive_data(fd);
|
||||
Data *delta_data = receive_data(fd);
|
||||
if (!delta_data) {
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
@@ -347,7 +177,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Data* raw_delta = delta_data;
|
||||
Data *raw_delta = delta_data;
|
||||
if (config->use_compression) {
|
||||
raw_delta = data_decompress(delta_data);
|
||||
data_destroy(delta_data);
|
||||
@@ -359,7 +189,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
}
|
||||
}
|
||||
|
||||
Delta* delta = delta_deserialize(raw_delta);
|
||||
Delta *delta = delta_deserialize(raw_delta);
|
||||
data_destroy(raw_delta);
|
||||
if (!delta) {
|
||||
free(old_data);
|
||||
@@ -368,7 +198,8 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_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);
|
||||
|
||||
@@ -379,7 +210,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
|
||||
File* file = file_create(check_path);
|
||||
File *file = file_create(check_path);
|
||||
if (!file) {
|
||||
free(new_data);
|
||||
free(old_data);
|
||||
@@ -413,22 +244,29 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
delta_signature_destroy(sig);
|
||||
free(old_data);
|
||||
|
||||
File* file = file_create(check_path);
|
||||
if (!file) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
File *file = file_create(check_path);
|
||||
if (!file) { 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); 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);
|
||||
Data *file_data = receive_data(fd);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (config->use_compression) {
|
||||
Data *uncompressed = data_decompress(file_data);
|
||||
data_destroy(file_data);
|
||||
if (uncompressed == NULL) { file_destroy(file); send_status(fd, STATUS_ERROR); return NULL; }
|
||||
file_data = uncompressed;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
@@ -439,13 +277,10 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
|
||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
File *receive_incremental_check(int fd, Config *config, bool *skipped) {
|
||||
*skipped = false;
|
||||
char* check_path = receive_str(fd);
|
||||
if (check_path == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
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;
|
||||
@@ -456,39 +291,17 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* full_path = path_cat(config->receive_root_directory, check_path);
|
||||
if (full_path && strstr(full_path, "..") != NULL) {
|
||||
log_message(LOG_LEVEL_ERROR, "Path traversal blocked: %s", full_path);
|
||||
free(full_path);
|
||||
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) {
|
||||
snprintf(partial_path, strlen(full_path) + 20, "%s.fastsync-partial", full_path);
|
||||
has_old_file = (stat(partial_path, &st) == 0);
|
||||
if (has_old_file)
|
||||
old_size = (unsigned long long)st.st_size;
|
||||
free(partial_path);
|
||||
}
|
||||
}
|
||||
|
||||
bool match = has_old_file && (unsigned long long)st.st_size == check_size &&
|
||||
bool match = has_old_file &&
|
||||
(unsigned long long)st.st_size == check_size &&
|
||||
(long long)st.st_mtime == check_mtime;
|
||||
|
||||
if (match) {
|
||||
if (!send_status(fd, STATUS_OK)) {
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
return NULL;
|
||||
}
|
||||
if (!send_status(fd, STATUS_OK)) { free(full_path); free(check_path); return NULL; }
|
||||
free(full_path);
|
||||
free(check_path);
|
||||
*skipped = true;
|
||||
@@ -499,8 +312,9 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
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);
|
||||
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);
|
||||
@@ -517,106 +331,65 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) {
|
||||
}
|
||||
}
|
||||
|
||||
File* file = file_create(check_path);
|
||||
File *file = file_create(check_path);
|
||||
free(check_path);
|
||||
free(full_path);
|
||||
if (file == NULL) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
if (file == NULL) { 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); send_status(fd, STATUS_ERROR); return NULL; }
|
||||
}
|
||||
|
||||
if (!receive_and_assign_metadata(fd, config, file))
|
||||
return NULL;
|
||||
|
||||
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);
|
||||
Data *file_data = receive_data(fd);
|
||||
if (file_data == NULL) {
|
||||
file_destroy(file);
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (config->use_compression) {
|
||||
Data *uncompressed = data_decompress(file_data);
|
||||
data_destroy(file_data);
|
||||
if (uncompressed == NULL) { file_destroy(file); send_status(fd, STATUS_ERROR); return NULL; }
|
||||
file_data = uncompressed;
|
||||
}
|
||||
|
||||
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;
|
||||
bool to_disk(const char *path, const void *data, unsigned long long data_size) {
|
||||
char *directory = str_dup(path);
|
||||
char *dir_to_free = directory;
|
||||
directory = dirname(directory);
|
||||
if (!mkdir_r(directory)) {
|
||||
ok = false;
|
||||
goto done;
|
||||
free(dir_to_free);
|
||||
return false;
|
||||
}
|
||||
FILE* file_pointer = fopen(path, "wb");
|
||||
FILE *file_pointer = fopen(path, "wb");
|
||||
if (file_pointer == NULL) {
|
||||
perror("Could not open File");
|
||||
ok = false;
|
||||
goto done;
|
||||
free(dir_to_free);
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
free(dir_to_free);
|
||||
return false;
|
||||
}
|
||||
fclose(file_pointer);
|
||||
|
||||
done:
|
||||
free(directory);
|
||||
return ok;
|
||||
free(dir_to_free);
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
int ft = (int)file->type;
|
||||
if (!send_int(file_descriptor, ft))
|
||||
return false;
|
||||
bool file_send_sendfile(File *file, int file_descriptor, bool use_metadata, int compression_level, bool send_path) {
|
||||
(void)compression_level;
|
||||
if (send_path && !send_str(file_descriptor, file->path)) return false;
|
||||
if (use_metadata && !metadata_send(file_descriptor, file->metadata)) return false;
|
||||
|
||||
int fd = open(file->path, O_RDONLY);
|
||||
if (fd == -1) {
|
||||
@@ -631,14 +404,9 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
|
||||
}
|
||||
|
||||
off_t offset = 0;
|
||||
while ((unsigned long long)offset < file_size) {
|
||||
size_t send_count = (size_t)(file_size - (unsigned long long)offset);
|
||||
if ((unsigned long long)send_count != file_size - (unsigned long long)offset)
|
||||
send_count = SIZE_MAX;
|
||||
ssize_t sent = sendfile(file_descriptor, fd, &offset, send_count);
|
||||
while (offset < file_size) {
|
||||
ssize_t sent = sendfile(file_descriptor, fd, &offset, file_size - offset);
|
||||
if (sent == -1) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
perror("sendfile failed");
|
||||
close(fd);
|
||||
return false;
|
||||
@@ -649,53 +417,24 @@ bool file_send_sendfile(File* file, int file_descriptor, bool use_metadata, int
|
||||
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);
|
||||
File *file_receive(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 (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;
|
||||
}
|
||||
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);
|
||||
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 *file_data_uncompressed = data_decompress(file_data);
|
||||
data_destroy(file_data);
|
||||
if (file_data_uncompressed == NULL) {
|
||||
file_destroy(file);
|
||||
@@ -703,19 +442,19 @@ File* file_receive(const Config* config, int file_descriptor) {
|
||||
}
|
||||
file_data = file_data_uncompressed;
|
||||
}
|
||||
|
||||
data_destroy(file->data);
|
||||
file->data = file_data;
|
||||
return file;
|
||||
}
|
||||
|
||||
size_t file_content_to_buffer(File* file) {
|
||||
FILE* file_pointer = fopen(file->path, "rb");
|
||||
size_t file_content_to_buffer(File *file) {
|
||||
FILE *file_pointer = fopen(file->path, "rb");
|
||||
if (file_pointer == NULL) {
|
||||
perror("Could not open the file!");
|
||||
return 0;
|
||||
}
|
||||
size_t bytes_read = fread(file->data->data, 1, file->data->size, file_pointer);
|
||||
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!");
|
||||
@@ -725,22 +464,21 @@ size_t file_content_to_buffer(File* file) {
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
int receive_manifest(int fd, const Config* config, int* next_status) {
|
||||
int receive_manifest(int fd, Config *config, int *next_status) {
|
||||
int count;
|
||||
if (!receive_int(fd, &count))
|
||||
return -1;
|
||||
ArrayList* manifest = array_list_create(free);
|
||||
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);
|
||||
char *s = receive_str(fd);
|
||||
if (s) array_list_add(manifest, s);
|
||||
}
|
||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||
delete_extras(config->receive_root_directory, manifest);
|
||||
array_list_delete(manifest);
|
||||
}
|
||||
if (!receive_status(fd, next_status))
|
||||
return -1;
|
||||
if (!receive_status(fd, next_status)) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+16
-22
@@ -6,8 +6,6 @@
|
||||
#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;
|
||||
@@ -17,27 +15,23 @@ typedef struct {
|
||||
} FileMetadata;
|
||||
|
||||
typedef struct {
|
||||
char* path;
|
||||
Data* data;
|
||||
FileMetadata* metadata;
|
||||
FileType type;
|
||||
char* link_target;
|
||||
char *path;
|
||||
Data *data;
|
||||
FileMetadata *metadata;
|
||||
} File;
|
||||
|
||||
File* file_create(const char* path);
|
||||
void file_destroy(void* item);
|
||||
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);
|
||||
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);
|
||||
File *file_create(const char *path);
|
||||
void file_destroy(void *item);
|
||||
bool file_load_data(File *file);
|
||||
File *file_receive(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);
|
||||
FileMetadata *file_metadata_create(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, Config *config, bool *skipped);
|
||||
int receive_manifest(int fd, Config *config, int *next_status);
|
||||
|
||||
#endif
|
||||
|
||||
+6
-8
@@ -3,24 +3,22 @@
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
|
||||
static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
||||
static const char *log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"};
|
||||
static LogLevel current_log_level = LOG_LEVEL_WARNING;
|
||||
|
||||
void set_log_level(LogLevel level) {
|
||||
current_log_level = level;
|
||||
}
|
||||
|
||||
void log_message(LogLevel log_level, const char* format, ...) {
|
||||
void log_message(LogLevel log_level, char *format, ...) {
|
||||
if (log_level < current_log_level)
|
||||
return;
|
||||
time_t now = time(NULL);
|
||||
struct tm result_buf;
|
||||
const struct tm* t = localtime_r(&now, &result_buf);
|
||||
if (t == NULL)
|
||||
return;
|
||||
struct tm *t = localtime(&now);
|
||||
|
||||
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]);
|
||||
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);
|
||||
|
||||
+7
-2
@@ -1,9 +1,14 @@
|
||||
#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, const char* message, ...);
|
||||
void log_message(LogLevel log_level, char *message, ...);
|
||||
void set_log_level(LogLevel level);
|
||||
|
||||
#endif
|
||||
|
||||
+48
-136
@@ -1,178 +1,90 @@
|
||||
#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>
|
||||
|
||||
/*
|
||||
* Wire format serialization (protocol version 2.0.0+):
|
||||
* All metadata fields are serialized as fixed-width integers (int32_t / int64_t)
|
||||
* to ensure cross-platform binary compatiblity. See metadata.h for the
|
||||
* exact wire layout.
|
||||
*
|
||||
* Compile-time assertions verify that the native platform types fit within
|
||||
* the chosen fixed-width representations.
|
||||
*/
|
||||
typedef char static_assert_mode_t_fits[(sizeof(mode_t) <= sizeof(int32_t)) ? 1 : -1];
|
||||
typedef char static_assert_uid_t_fits[(sizeof(uid_t) <= sizeof(int32_t)) ? 1 : -1];
|
||||
typedef char static_assert_gid_t_fits[(sizeof(gid_t) <= sizeof(int32_t)) ? 1 : -1];
|
||||
|
||||
void metadata_to_buf(char** buf, const FileMetadata* m) {
|
||||
int32_t present = (m != NULL) ? 1 : 0;
|
||||
memcpy(*buf, &present, sizeof(present));
|
||||
*buf += sizeof(present);
|
||||
void metadata_to_buf(char **buf, FileMetadata *m) {
|
||||
int present = (m != NULL) ? 1 : 0;
|
||||
memcpy(*buf, &present, sizeof(int));
|
||||
*buf += sizeof(int);
|
||||
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);
|
||||
memcpy(*buf, &m->mode, sizeof(mode_t)); *buf += sizeof(mode_t);
|
||||
memcpy(*buf, &m->uid, sizeof(uid_t)); *buf += sizeof(uid_t);
|
||||
memcpy(*buf, &m->gid, sizeof(gid_t)); *buf += sizeof(gid_t);
|
||||
memcpy(*buf, &m->mtime_sec, sizeof(time_t)); *buf += sizeof(time_t);
|
||||
memcpy(*buf, &m->mtime_nsec, sizeof(long)); *buf += sizeof(long);
|
||||
}
|
||||
|
||||
FileMetadata* metadata_from_buf(char** buf) {
|
||||
int32_t present;
|
||||
memcpy(&present, *buf, sizeof(present));
|
||||
*buf += sizeof(present);
|
||||
FileMetadata *metadata_from_buf(char **buf) {
|
||||
int present;
|
||||
memcpy(&present, *buf, sizeof(int));
|
||||
*buf += sizeof(int);
|
||||
if (!present)
|
||||
return NULL;
|
||||
FileMetadata* m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL)
|
||||
return NULL;
|
||||
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;
|
||||
FileMetadata *m = malloc(sizeof(FileMetadata));
|
||||
memcpy(&m->mode, *buf, sizeof(mode_t)); *buf += sizeof(mode_t);
|
||||
memcpy(&m->uid, *buf, sizeof(uid_t)); *buf += sizeof(uid_t);
|
||||
memcpy(&m->gid, *buf, sizeof(gid_t)); *buf += sizeof(gid_t);
|
||||
memcpy(&m->mtime_sec, *buf, sizeof(time_t)); *buf += sizeof(time_t);
|
||||
memcpy(&m->mtime_nsec, *buf, sizeof(long)); *buf += sizeof(long);
|
||||
return m;
|
||||
}
|
||||
|
||||
bool metadata_send(int file_descriptor, FileMetadata* 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));
|
||||
int zero = 0;
|
||||
return send_n_data(file_descriptor, &zero, sizeof(int));
|
||||
}
|
||||
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));
|
||||
int present = 1;
|
||||
return send_n_data(file_descriptor, &present, sizeof(int)) &&
|
||||
send_n_data(file_descriptor, &m->mode, sizeof(mode_t)) &&
|
||||
send_n_data(file_descriptor, &m->uid, sizeof(uid_t)) &&
|
||||
send_n_data(file_descriptor, &m->gid, sizeof(gid_t)) &&
|
||||
send_n_data(file_descriptor, &m->mtime_sec, sizeof(time_t)) &&
|
||||
send_n_data(file_descriptor, &m->mtime_nsec, sizeof(long));
|
||||
}
|
||||
|
||||
FileMetadata* metadata_receive(int file_descriptor, int* ok) {
|
||||
int32_t present;
|
||||
if (!receive_n_data(file_descriptor, &present, sizeof(present))) {
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
FileMetadata *metadata_receive(int file_descriptor, int *ok) {
|
||||
int present;
|
||||
if (!receive_n_data(file_descriptor, &present, sizeof(int))) {
|
||||
if (ok) *ok = 0;
|
||||
return NULL;
|
||||
}
|
||||
if (!present) {
|
||||
if (ok)
|
||||
*ok = 1;
|
||||
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))) {
|
||||
FileMetadata *m = malloc(sizeof(FileMetadata));
|
||||
if (m == NULL) { if (ok) *ok = 0; return NULL; }
|
||||
if (!receive_n_data(file_descriptor, &m->mode, sizeof(mode_t)) ||
|
||||
!receive_n_data(file_descriptor, &m->uid, sizeof(uid_t)) ||
|
||||
!receive_n_data(file_descriptor, &m->gid, sizeof(gid_t)) ||
|
||||
!receive_n_data(file_descriptor, &m->mtime_sec, sizeof(time_t)) ||
|
||||
!receive_n_data(file_descriptor, &m->mtime_nsec, sizeof(long))) {
|
||||
free(m);
|
||||
if (ok)
|
||||
*ok = 0;
|
||||
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;
|
||||
if (ok) *ok = 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
void file_restore_metadata(const char* path, FileMetadata* metadata) {
|
||||
void file_restore_metadata(const char *path, FileMetadata *metadata) {
|
||||
if (metadata == NULL)
|
||||
return;
|
||||
if (chmod(path, metadata->mode & 07777 & ~(S_ISUID | S_ISGID)) != 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));
|
||||
chmod(path, metadata->mode & 07777);
|
||||
int chown_ret = chown(path, metadata->uid, metadata->gid);
|
||||
(void)chown_ret;
|
||||
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));
|
||||
utimensat(AT_FDCWD, path, times, 0);
|
||||
}
|
||||
|
||||
+6
-24
@@ -3,32 +3,14 @@
|
||||
|
||||
#include "file.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/*
|
||||
* Wire format (introduced in protocol version 2.0.0):
|
||||
* int32_t present
|
||||
* int32_t mode (was mode_t, platform-dependent)
|
||||
* int32_t uid (was uid_t, platform-dependent)
|
||||
* int32_t gid (was gid_t, platform-dependent)
|
||||
* int64_t mtime_sec (was time_t, platform-dependent)
|
||||
* int64_t mtime_nsec (was long, platform-dependent)
|
||||
*
|
||||
* Prior to 2.0.0 the wire format used the raw platform-dependent types,
|
||||
* which broke compatiblity across different systems. All fields are now
|
||||
* serialized as fixed-width integers.
|
||||
*/
|
||||
#define FILE_METADATA_WIRE_SIZE (sizeof(mode_t) + sizeof(uid_t) + sizeof(gid_t) + sizeof(time_t) + sizeof(long))
|
||||
|
||||
/* Size of metadata fields on wire, excluding the int32_t `present` field that
|
||||
* is always sent first. The total wire size for present metadata is
|
||||
* sizeof(int32_t) + FILE_METADATA_WIRE_SIZE (32 bytes on most platforms). */
|
||||
#define FILE_METADATA_WIRE_SIZE (sizeof(int32_t) * 3 + sizeof(int64_t) * 2)
|
||||
|
||||
void metadata_to_buf(char** buf, const FileMetadata* m);
|
||||
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);
|
||||
void metadata_to_buf(char **buf, 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
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
#include <string.h>
|
||||
#include <threads.h>
|
||||
|
||||
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,
|
||||
Queue* queue_loader) {
|
||||
PipelineContextSender* context = malloc(sizeof(PipelineContextSender));
|
||||
if (context == NULL)
|
||||
return NULL;
|
||||
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;
|
||||
@@ -37,7 +37,7 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que
|
||||
return context;
|
||||
}
|
||||
|
||||
void pipeline_context_sender_destroy(PipelineContextSender* context) {
|
||||
void pipeline_context_sender_destroy(PipelineContextSender *context) {
|
||||
if (context->manifest) {
|
||||
array_list_delete(context->manifest);
|
||||
}
|
||||
@@ -53,11 +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;
|
||||
PipelineContextReceiver *context = malloc(sizeof(PipelineContextReceiver));
|
||||
if (context == NULL) return NULL;
|
||||
context->config = config;
|
||||
context->queue = queue;
|
||||
context->file_descriptor = file_descriptor;
|
||||
@@ -72,7 +72,7 @@ PipelineContextReceiver* pipeline_context_receiver_create(Config* config, Queue*
|
||||
return context;
|
||||
}
|
||||
|
||||
void pipeline_context_receiver_destroy(PipelineContextReceiver* context) {
|
||||
void pipeline_context_receiver_destroy(PipelineContextReceiver *context) {
|
||||
config_delete(context->config);
|
||||
queue_destroy(context->queue);
|
||||
mtx_destroy(&context->mutex);
|
||||
@@ -81,57 +81,57 @@ void pipeline_context_receiver_destroy(PipelineContextReceiver* context) {
|
||||
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;
|
||||
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];
|
||||
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);
|
||||
&context->condition_not_empty,
|
||||
&context->condition_not_full);
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
}
|
||||
|
||||
int receive_thread(void* pipeline_context) {
|
||||
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
||||
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;
|
||||
Config *config = context->config;
|
||||
mtx_unlock(&context->mutex);
|
||||
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
return thrd_error;
|
||||
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);
|
||||
File *file = receive_incremental_check(file_descriptor, config, &skipped);
|
||||
if (!skipped) {
|
||||
if (file == NULL)
|
||||
return thrd_error;
|
||||
if (file == NULL) return thrd_error;
|
||||
queue_enqueue_multithreaded(context->queue, file, &context->mutex,
|
||||
&context->condition_not_empty, &context->condition_not_full);
|
||||
&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);
|
||||
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);
|
||||
&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 (!receive_status(file_descriptor, &status)) return thrd_error;
|
||||
}
|
||||
if (status == STATUS_MANIFEST) {
|
||||
if (receive_manifest(file_descriptor, config, &status) != 0)
|
||||
return thrd_error;
|
||||
if (receive_manifest(file_descriptor, config, &status) != 0) return thrd_error;
|
||||
}
|
||||
mtx_lock(&context->mutex);
|
||||
context->receiver_done = true;
|
||||
@@ -140,17 +140,18 @@ int receive_thread(void* pipeline_context) {
|
||||
return thrd_success;
|
||||
}
|
||||
|
||||
int write_thread(void* pipeline_context) {
|
||||
PipelineContextReceiver* context = (PipelineContextReceiver*)pipeline_context;
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
|
||||
@@ -9,23 +9,23 @@
|
||||
#include "queue.h"
|
||||
|
||||
typedef struct {
|
||||
Config* config;
|
||||
Queue* queue_scanner;
|
||||
Config *config;
|
||||
Queue *queue_scanner;
|
||||
mtx_t mutex_scanner;
|
||||
cnd_t condition_not_full_scanner;
|
||||
cnd_t condition_not_empty_scanner;
|
||||
bool scanner_done;
|
||||
Queue* queue_loader;
|
||||
Queue *queue_loader;
|
||||
mtx_t mutex_loader;
|
||||
cnd_t condition_not_full_loader;
|
||||
cnd_t condition_not_empty_loader;
|
||||
bool loader_done;
|
||||
ArrayList* manifest;
|
||||
ArrayList *manifest;
|
||||
} PipelineContextSender;
|
||||
|
||||
typedef struct PipelineContextReceiver {
|
||||
Queue* queue;
|
||||
Config* config;
|
||||
Queue *queue;
|
||||
Config *config;
|
||||
int file_descriptor;
|
||||
mtx_t mutex;
|
||||
cnd_t condition_not_full;
|
||||
@@ -33,12 +33,14 @@ typedef struct PipelineContextReceiver {
|
||||
bool receiver_done;
|
||||
} PipelineContextReceiver;
|
||||
|
||||
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,
|
||||
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,
|
||||
int file_descriptor);
|
||||
void pipeline_context_receiver_destroy(PipelineContextReceiver* context);
|
||||
int receive_thread(void* pipeline_context);
|
||||
int write_thread(void* pipeline_context);
|
||||
void pipeline_context_receiver_destroy(PipelineContextReceiver *context);
|
||||
int receive_thread(void *pipeline_context);
|
||||
int write_thread(void *pipeline_context);
|
||||
#endif
|
||||
|
||||
+34
-69
@@ -10,7 +10,7 @@
|
||||
|
||||
static __thread int io_read_fd = -1;
|
||||
static __thread int io_write_fd = -1;
|
||||
static SSL* io_ssl = NULL;
|
||||
static SSL *io_ssl = NULL;
|
||||
|
||||
static unsigned long long io_bwlimit = 0;
|
||||
static long long bw_tokens = 0;
|
||||
@@ -28,14 +28,13 @@ void io_set_bwlimit(unsigned long long bytes_per_sec) {
|
||||
}
|
||||
|
||||
static void bw_throttle(size_t bytes_written) {
|
||||
if (io_bwlimit == 0)
|
||||
return;
|
||||
if (io_bwlimit == 0) return;
|
||||
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
|
||||
long long elapsed_ns =
|
||||
(now.tv_sec - bw_last_refill.tv_sec) * 1000000000LL + (now.tv_nsec - bw_last_refill.tv_nsec);
|
||||
long long elapsed_ns = (now.tv_sec - bw_last_refill.tv_sec) * 1000000000LL +
|
||||
(now.tv_nsec - bw_last_refill.tv_nsec);
|
||||
bw_last_refill = now;
|
||||
|
||||
long long tokens_to_add = (long long)((double)io_bwlimit * elapsed_ns / 1000000000.0);
|
||||
@@ -57,7 +56,7 @@ static void bw_throttle(size_t bytes_written) {
|
||||
}
|
||||
}
|
||||
|
||||
void io_set_ssl(SSL* ssl) {
|
||||
void io_set_ssl(SSL *ssl) {
|
||||
io_ssl = ssl;
|
||||
}
|
||||
|
||||
@@ -65,25 +64,20 @@ 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) {
|
||||
bool send_n_data(int file_descriptor, 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) {
|
||||
while (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);
|
||||
bytes_send = SSL_write(io_ssl, (char *)data + total_bytes_send, chunk);
|
||||
else
|
||||
bytes_send = write(fd, (const char*)data + total_bytes_send, chunk);
|
||||
bytes_send = write(fd, (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;
|
||||
}
|
||||
@@ -94,24 +88,19 @@ bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
|
||||
bool receive_n_data(int file_descriptor, void *data, size_t data_size) {
|
||||
log_message(LOG_LEVEL_DEBUG, " Receiving n Data: %zu", data_size);
|
||||
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);
|
||||
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);
|
||||
bytes_received = read(fd, (char *)data + total_bytes_received,
|
||||
data_size - total_bytes_received);
|
||||
if (bytes_received <= 0) {
|
||||
if (io_ssl && bytes_received < 0) {
|
||||
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
|
||||
@@ -124,7 +113,7 @@ bool receive_n_data(int file_descriptor, void* data, size_t data_size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char* status_to_string(Status status) {
|
||||
static const char *status_to_string(Status status) {
|
||||
switch (status) {
|
||||
case STATUS_OK:
|
||||
return "OK";
|
||||
@@ -147,32 +136,19 @@ static const char* status_to_string(Status status) {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
bool send_str(int file_descriptor, char *data) {
|
||||
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;
|
||||
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) {
|
||||
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, &size, sizeof(size_t))) 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;
|
||||
@@ -182,61 +158,50 @@ char* receive_str(int file_descriptor) {
|
||||
return data;
|
||||
}
|
||||
|
||||
bool send_data(int file_descriptor, const Data* data) {
|
||||
bool send_data(int file_descriptor, 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 %llu data", data_size);
|
||||
log_message(LOG_LEVEL_DEBUG, "Send %lld data", data_size);
|
||||
return true;
|
||||
}
|
||||
|
||||
#define MAX_DATA_SIZE (1024ULL * 1024 * 1024)
|
||||
|
||||
Data* receive_data(int file_descriptor) {
|
||||
Data *receive_data(int file_descriptor) {
|
||||
unsigned long long size = 0;
|
||||
if (!receive_n_data(file_descriptor, &size, sizeof(unsigned long long)))
|
||||
return NULL;
|
||||
if ((size_t)size != size || size > MAX_DATA_SIZE) {
|
||||
log_message(LOG_LEVEL_ERROR, "receive_data size %llu exceeds limits", size);
|
||||
return NULL;
|
||||
}
|
||||
void* data = malloc((size_t)size);
|
||||
if (data == NULL)
|
||||
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 %llu data", size);
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
+11
-23
@@ -5,37 +5,25 @@
|
||||
#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
|
||||
};
|
||||
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);
|
||||
typedef struct ssl_st SSL;
|
||||
void io_set_ssl(SSL *ssl);
|
||||
bool send_n_data(int file_descriptor, 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_str(int file_descriptor, char *data);
|
||||
char *receive_str(int file_descriptor);
|
||||
bool send_data(int file_descriptor, 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 receive_int(int file_descriptor, int *data);
|
||||
bool send_status(int file_descriptor, Status status);
|
||||
bool receive_status(int file_descriptor, Status* status);
|
||||
bool receive_status(int file_descriptor, Status *status);
|
||||
|
||||
#endif
|
||||
|
||||
+22
-22
@@ -6,14 +6,14 @@
|
||||
|
||||
#include "queue.h"
|
||||
|
||||
Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
|
||||
Queue* queue = (Queue*)malloc(sizeof(Queue));
|
||||
Queue *queue_create(int capacity, void (*destroyer)(void *item)) {
|
||||
Queue *queue = (Queue *)malloc(sizeof(Queue));
|
||||
if (queue == NULL) {
|
||||
perror("ERROR: Could not allocate memory for queue structure");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
queue->items = malloc(capacity * sizeof(void*));
|
||||
queue->items = malloc(capacity * sizeof(void *));
|
||||
if (queue->items == NULL) {
|
||||
free(queue);
|
||||
return NULL;
|
||||
@@ -32,7 +32,7 @@ Queue* queue_create(int capacity, void (*destroyer)(void* item)) {
|
||||
return queue;
|
||||
}
|
||||
|
||||
void queue_destroy(Queue* queue) {
|
||||
void queue_destroy(Queue *queue) {
|
||||
if (queue == NULL)
|
||||
return;
|
||||
|
||||
@@ -46,25 +46,24 @@ void queue_destroy(Queue* queue) {
|
||||
free(queue);
|
||||
}
|
||||
|
||||
bool queue_is_empty(const Queue* queue) {
|
||||
bool queue_is_empty(Queue *queue) {
|
||||
if (queue == NULL)
|
||||
return true;
|
||||
return queue->size == 0;
|
||||
}
|
||||
|
||||
bool queue_is_full(const Queue* queue) {
|
||||
bool queue_is_full(Queue *queue) {
|
||||
if (queue == NULL)
|
||||
return false;
|
||||
return queue->size == queue->capacity;
|
||||
}
|
||||
|
||||
static bool queue_double_capacity(Queue* queue) {
|
||||
if (queue == NULL)
|
||||
return false;
|
||||
static bool queue_double_capacity(Queue *queue) {
|
||||
if (queue == NULL) return false;
|
||||
unsigned int new_capacity = queue->capacity * 2;
|
||||
if (new_capacity <= 1)
|
||||
new_capacity = 100;
|
||||
void** new_items = malloc(new_capacity * sizeof(void*));
|
||||
void **new_items = malloc(new_capacity * sizeof(void *));
|
||||
if (new_items == NULL) {
|
||||
perror("ERROR: Could not allocate memory for doubling capacity of queue.");
|
||||
return false;
|
||||
@@ -79,12 +78,10 @@ static bool queue_double_capacity(Queue* queue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool queue_enqueue(Queue* queue, void* item) {
|
||||
if (queue == NULL || item == NULL)
|
||||
return false;
|
||||
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_double_capacity(queue)) return false;
|
||||
}
|
||||
queue->items[queue->rear] = item;
|
||||
queue->rear = (queue->rear + 1) % queue->capacity;
|
||||
@@ -92,8 +89,9 @@ bool queue_enqueue(Queue* queue, void* item) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full) {
|
||||
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);
|
||||
@@ -103,21 +101,23 @@ bool queue_enqueue_multithreaded(Queue* queue, void* item, mtx_t* mutex, cnd_t*
|
||||
return ok;
|
||||
}
|
||||
|
||||
void* queue_dequeue(Queue* queue) {
|
||||
void *queue_dequeue(Queue *queue) {
|
||||
if (queue == NULL || queue_is_empty(queue)) {
|
||||
perror("ERROR: Could not dequeue from null or empty queue.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void* item = queue->items[queue->front];
|
||||
void *item = queue->items[queue->front];
|
||||
queue->items[queue->front] = NULL;
|
||||
queue->front = (queue->front + 1) % queue->capacity;
|
||||
queue->size--;
|
||||
return item;
|
||||
}
|
||||
|
||||
void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_not_empty,
|
||||
cnd_t* condition_not_full, const bool* other_thread_done) {
|
||||
void *queue_dequeue_multithreaded(Queue *queue, mtx_t *mutex,
|
||||
cnd_t *condition_not_empty,
|
||||
cnd_t *condition_not_full,
|
||||
bool *other_thread_done) {
|
||||
mtx_lock(mutex);
|
||||
while (queue_is_empty(queue) && !*other_thread_done)
|
||||
cnd_wait(condition_not_empty, mutex);
|
||||
@@ -125,7 +125,7 @@ void* queue_dequeue_multithreaded(Queue* queue, mtx_t* mutex, cnd_t* condition_n
|
||||
mtx_unlock(mutex);
|
||||
return NULL;
|
||||
}
|
||||
void* item = queue_dequeue(queue);
|
||||
void *item = queue_dequeue(queue);
|
||||
cnd_signal(condition_not_full);
|
||||
mtx_unlock(mutex);
|
||||
return item;
|
||||
|
||||
+15
-12
@@ -5,23 +5,26 @@
|
||||
#include <threads.h>
|
||||
|
||||
typedef struct Queue {
|
||||
void** items;
|
||||
void **items;
|
||||
int front;
|
||||
int rear;
|
||||
int size;
|
||||
int capacity;
|
||||
void (*item_destroyer)(void* item);
|
||||
void (*item_destroyer)(void *item);
|
||||
} Queue;
|
||||
|
||||
Queue* queue_create(int capacity, void (*destroyer)(void* item));
|
||||
void queue_destroy(Queue* queue);
|
||||
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, const bool* other_thread_done);
|
||||
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);
|
||||
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);
|
||||
|
||||
#endif
|
||||
|
||||
+28
-76
@@ -1,5 +1,4 @@
|
||||
#include "transport_ssh.h"
|
||||
#include "utils.h"
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -9,65 +8,41 @@
|
||||
#include <unistd.h>
|
||||
|
||||
typedef struct {
|
||||
char* user;
|
||||
char* host;
|
||||
char* remote_path;
|
||||
char user[256];
|
||||
char host[256];
|
||||
char remote_path[4096];
|
||||
} 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) {
|
||||
const char *colon = strchr(dest, ':');
|
||||
if (!colon) return -1;
|
||||
|
||||
static int parse_remote_dest(const char* dest, RemoteDest* r) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
const char* colon = strchr(dest, ':');
|
||||
if (!colon)
|
||||
return -1;
|
||||
size_t remote_path_len = strlen(colon + 1);
|
||||
if (remote_path_len >= sizeof(r->remote_path)) return -1;
|
||||
memcpy(r->remote_path, colon + 1, remote_path_len + 1);
|
||||
|
||||
r->remote_path = str_dup(colon + 1);
|
||||
if (!r->remote_path)
|
||||
return -1;
|
||||
|
||||
const char* at = memchr(dest, '@', colon - dest);
|
||||
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;
|
||||
}
|
||||
if (user_len >= sizeof(r->user)) 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;
|
||||
}
|
||||
if (host_len >= sizeof(r->host)) 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;
|
||||
}
|
||||
r->user[0] = '\0';
|
||||
size_t host_len = colon - dest;
|
||||
r->host = malloc(host_len + 1);
|
||||
if (!r->host) {
|
||||
remote_dest_destroy(r);
|
||||
return -1;
|
||||
}
|
||||
if (host_len >= sizeof(r->host)) return -1;
|
||||
memcpy(r->host, dest, host_len);
|
||||
r->host[host_len] = '\0';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Client* client_connect_ssh(const char* destination, int port, const char* server_path) {
|
||||
Client *client_connect_ssh(char *destination, int port) {
|
||||
RemoteDest r;
|
||||
if (parse_remote_dest(destination, &r) != 0) {
|
||||
fprintf(stderr, "Invalid remote destination: %s\n", destination);
|
||||
@@ -77,7 +52,6 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) {
|
||||
perror("socketpair failed");
|
||||
remote_dest_destroy(&r);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -90,20 +64,15 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
|
||||
int exec_pipe[2];
|
||||
if (pipe(exec_pipe) < 0) {
|
||||
perror("pipe failed");
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
remote_dest_destroy(&r);
|
||||
close(sv[0]); close(sv[1]);
|
||||
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);
|
||||
close(sv[0]); close(sv[1]);
|
||||
close(exec_pipe[0]); close(exec_pipe[1]);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -111,26 +80,20 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
|
||||
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]);
|
||||
if (sv[1] > 1) close(sv[1]);
|
||||
|
||||
char ssh_user[512];
|
||||
int needed;
|
||||
if (r.user && r.user[0] != '\0')
|
||||
needed = snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
|
||||
if (r.user[0] != '\0')
|
||||
snprintf(ssh_user, sizeof(ssh_user), "%s@%s", r.user, r.host);
|
||||
else
|
||||
needed = snprintf(ssh_user, sizeof(ssh_user), "%s", r.host);
|
||||
if ((size_t)needed >= sizeof(ssh_user))
|
||||
fprintf(stderr, "Warning: ssh_user string truncated\n");
|
||||
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);
|
||||
char *ssh_argv[16];
|
||||
int ac = 0;
|
||||
char port_str[16];
|
||||
ssh_argv[ac++] = "ssh";
|
||||
@@ -141,22 +104,15 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
|
||||
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) {
|
||||
_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) {
|
||||
_exit(1);
|
||||
}
|
||||
ssh_argv[ac++] = ssh_user;
|
||||
ssh_argv[ac++] = (char*)(server_path ? server_path : "fastsync-server");
|
||||
ssh_argv[ac++] = "fastsync-server";
|
||||
ssh_argv[ac++] = "--stdio";
|
||||
ssh_argv[ac] = NULL;
|
||||
execvp("ssh", ssh_argv);
|
||||
free(ssh_argv);
|
||||
perror("exec of ssh failed");
|
||||
ssize_t wret = write(exec_pipe[1], "x", 1);
|
||||
(void)wret;
|
||||
@@ -173,22 +129,18 @@ Client* client_connect_ssh(const char* destination, int port, const char* server
|
||||
if (n > 0) {
|
||||
close(sv[0]);
|
||||
waitpid(pid, NULL, 0);
|
||||
remote_dest_destroy(&r);
|
||||
fprintf(stderr, "Error: could not launch '%s --stdio' on remote\n",
|
||||
server_path ? server_path : "fastsync-server");
|
||||
fprintf(stderr, "Error: could not launch 'fastsync-server --stdio' on remote\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
remote_dest_destroy(&r);
|
||||
|
||||
Client* client = malloc(sizeof(Client));
|
||||
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.sin_family = AF_UNIX;
|
||||
client->address_length = 0;
|
||||
client->ssh_child_pid = pid;
|
||||
client->ssl = NULL;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
#include "transport_tcp.h"
|
||||
|
||||
Client* client_connect_ssh(const char* destination, int port, const char* server_path);
|
||||
Client *client_connect_ssh(char *destination, int port);
|
||||
|
||||
#endif
|
||||
|
||||
+58
-197
@@ -2,138 +2,57 @@
|
||||
#include "log.h"
|
||||
#include "protocol.h"
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.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));
|
||||
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);
|
||||
sa_family_t domain = AF_INET6;
|
||||
if (fd < 0) {
|
||||
fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
domain = AF_INET;
|
||||
}
|
||||
if (fd < 0) {
|
||||
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (file_descriptor < 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;
|
||||
server->ssl_ctx = NULL;
|
||||
server->file_descriptor = file_descriptor;
|
||||
int opt = 1;
|
||||
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
|
||||
if (setsockopt(server->file_descriptor, SOL_SOCKET, SO_REUSEADDR, &opt,
|
||||
sizeof(opt))) {
|
||||
perror("Error setting a socket option!");
|
||||
close(server->file_descriptor);
|
||||
free(server);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Use the domain from the socket we actually created
|
||||
struct sockaddr_storage* addr = &server->address;
|
||||
struct sockaddr_in* addr4 = (struct sockaddr_in*)addr;
|
||||
struct sockaddr_in6* addr6 = (struct sockaddr_in6*)addr;
|
||||
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);
|
||||
server->ssl_ctx = NULL;
|
||||
|
||||
if (domain == AF_INET6) {
|
||||
addr6->sin6_family = AF_INET6;
|
||||
addr6->sin6_addr = in6addr_any;
|
||||
addr6->sin6_port = htons(port);
|
||||
addr->ss_family = AF_INET6;
|
||||
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);
|
||||
addr->ss_family = AF_INET;
|
||||
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 (domain == 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;
|
||||
}
|
||||
if (bind(server->file_descriptor, (struct sockaddr *)&server->address,
|
||||
server->address_length) < 0) {
|
||||
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;
|
||||
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);
|
||||
@@ -143,36 +62,22 @@ void server_delete(Server** server) {
|
||||
*server = NULL;
|
||||
}
|
||||
|
||||
/* Flag set by server_request_shutdown() to request graceful shutdown
|
||||
of the accept loop. Accessed only from transport_tcp.c so it won't
|
||||
cause linker errors when this file is compiled into client/test targets. */
|
||||
static volatile sig_atomic_t g_tcp_cleanup_requested = 0;
|
||||
|
||||
void server_request_shutdown(void) {
|
||||
g_tcp_cleanup_requested = 1;
|
||||
}
|
||||
|
||||
static void accept_loop(Server* server, void (*child_fn)(int, void*), void* child_ctx,
|
||||
const char* log_fmt) {
|
||||
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 (!g_tcp_cleanup_requested) {
|
||||
struct sockaddr_storage client_addr;
|
||||
while (1) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
int fd = accept(server->file_descriptor, (struct sockaddr*)&client_addr, &client_len);
|
||||
int fd = accept(server->file_descriptor, (struct sockaddr *)&client_addr,
|
||||
&client_len);
|
||||
if (fd < 0) {
|
||||
if (errno == EINTR) {
|
||||
if (g_tcp_cleanup_requested)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
@@ -185,114 +90,72 @@ static void accept_loop(Server* server, void (*child_fn)(int, void*), void* chil
|
||||
}
|
||||
}
|
||||
|
||||
struct plain_ctx {
|
||||
void (*handler)(int);
|
||||
};
|
||||
struct plain_ctx { void (*handler)(int); };
|
||||
|
||||
static void plain_child_fn(int fd, void* ctx) {
|
||||
((struct plain_ctx*)ctx)->handler(fd);
|
||||
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);
|
||||
bool server_listen(Server *server, void (*handler)(int file_descriptor)) {
|
||||
log_message(LOG_LEVEL_INFO, "Start Listening on Port: %d",
|
||||
ntohs(server->address.sin_port));
|
||||
struct 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);
|
||||
void server_accept_loop(Server *server, void (*child_fn)(int, void *),
|
||||
void *child_ctx, const char *log_fmt) {
|
||||
log_message(LOG_LEVEL_INFO, "Start TLS Listening on Port: %d",
|
||||
ntohs(server->address.sin_port));
|
||||
accept_loop(server, child_fn, child_ctx, log_fmt);
|
||||
}
|
||||
|
||||
Client* client_create() {
|
||||
Client* client = (Client*)malloc(sizeof(Client));
|
||||
if (client == NULL) {
|
||||
Client *client_create() {
|
||||
int file_descriptor = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (file_descriptor < 0) {
|
||||
perror("Could not create Socket!");
|
||||
return NULL;
|
||||
}
|
||||
memset(&client->address, 0, sizeof(client->address));
|
||||
client->address.ss_family = AF_UNSPEC;
|
||||
|
||||
Client *client = (Client *)malloc(sizeof(Client));
|
||||
if (client == NULL) {
|
||||
close(file_descriptor);
|
||||
return NULL;
|
||||
}
|
||||
client->file_descriptor = file_descriptor;
|
||||
client->address.sin_family = AF_INET;
|
||||
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;
|
||||
bool client_connect(Client *client, char *host, int port) {
|
||||
client->address.sin_port = htons(port);
|
||||
|
||||
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));
|
||||
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) {
|
||||
perror("Could not convert host address!");
|
||||
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) {
|
||||
if (connect(client->file_descriptor, (struct sockaddr *)&client->address,
|
||||
client->address_length) < 0) {
|
||||
perror("Could not connect to Server!");
|
||||
freeaddrinfo(res);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save the connected address
|
||||
socklen_t addr_len = rp->ai_addrlen;
|
||||
if (addr_len > sizeof(client->address))
|
||||
addr_len = sizeof(client->address);
|
||||
memcpy(&client->address, rp->ai_addr, addr_len);
|
||||
client->address_length = addr_len;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
close(client->file_descriptor);
|
||||
if (client->ssh_child_pid > 0) {
|
||||
int status;
|
||||
waitpid(client->ssh_child_pid, &status, 0);
|
||||
@@ -300,10 +163,8 @@ void client_disconnect(Client* client) {
|
||||
}
|
||||
}
|
||||
|
||||
void client_delete(Client* client) {
|
||||
if (client == NULL)
|
||||
return;
|
||||
client_disconnect(client);
|
||||
void client_delete(Client *client) {
|
||||
if (client == NULL) return;
|
||||
if (client->ssl_ctx) {
|
||||
SSL_CTX_free(client->ssl_ctx);
|
||||
client->ssl_ctx = NULL;
|
||||
|
||||
+14
-18
@@ -1,38 +1,34 @@
|
||||
#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;
|
||||
struct sockaddr_in address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
void* ssl_ctx;
|
||||
void *ssl_ctx;
|
||||
} Server;
|
||||
|
||||
typedef struct Client {
|
||||
struct sockaddr_storage address;
|
||||
struct sockaddr_in address;
|
||||
unsigned int address_length;
|
||||
int file_descriptor;
|
||||
pid_t ssh_child_pid;
|
||||
void* ssl;
|
||||
void* ssl_ctx;
|
||||
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);
|
||||
void server_request_shutdown(void);
|
||||
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);
|
||||
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);
|
||||
|
||||
#endif
|
||||
|
||||
+37
-57
@@ -5,8 +5,6 @@
|
||||
#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>
|
||||
@@ -33,10 +31,11 @@ static void log_ssl_errors(void) {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
@@ -59,7 +58,8 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
||||
return NULL;
|
||||
}
|
||||
if (!SSL_CTX_check_private_key(ctx)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Private key does not match certificate");
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"Private key does not match certificate");
|
||||
SSL_CTX_free(ctx);
|
||||
return NULL;
|
||||
}
|
||||
@@ -74,25 +74,18 @@ static SSL_CTX* create_ssl_ctx(bool is_server, const char* cert, const char* key
|
||||
}
|
||||
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
|
||||
SSL_CTX_set_verify_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);
|
||||
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);
|
||||
@@ -100,44 +93,32 @@ static SSL* wrap_fd_with_ssl(int fd, SSL_CTX* ctx, bool is_server) {
|
||||
ret = SSL_connect(ssl);
|
||||
|
||||
if (ret <= 0) {
|
||||
log_message(LOG_LEVEL_ERROR, "SSL %s failed", is_server ? "accept" : "connect");
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
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);
|
||||
@@ -145,37 +126,36 @@ static void tls_child_fn(int fd, void* arg) {
|
||||
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};
|
||||
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))
|
||||
bool client_connect_tls(Client *client, char *host, int port,
|
||||
const char *cert_path, const char *key_path,
|
||||
const char *ca_path) {
|
||||
client->address.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, host, &client->address.sin_addr) <= 0) {
|
||||
perror("Could not convert host address!");
|
||||
return false;
|
||||
}
|
||||
if (connect(client->file_descriptor, (struct sockaddr *)&client->address,
|
||||
client->address_length) < 0) {
|
||||
perror("Could not connect to Server!");
|
||||
return false;
|
||||
}
|
||||
|
||||
SSL_CTX* ctx = create_ssl_ctx(false, cert_path, key_path, ca_path);
|
||||
if (!ctx)
|
||||
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);
|
||||
SSL *ssl = wrap_fd_with_ssl(client->file_descriptor, ctx, false);
|
||||
if (!ssl) {
|
||||
SSL_CTX_free(ctx);
|
||||
client->ssl_ctx = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set SNI and enable hostname verification
|
||||
SSL_set_tlsext_host_name(ssl, host);
|
||||
X509_VERIFY_PARAM* param = SSL_get0_param(ssl);
|
||||
if (param) {
|
||||
X509_VERIFY_PARAM_set1_host(param, host, 0);
|
||||
}
|
||||
|
||||
client->ssl = ssl;
|
||||
io_set_ssl(ssl);
|
||||
return true;
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
|
||||
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);
|
||||
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
|
||||
|
||||
+37
-100
@@ -2,49 +2,33 @@
|
||||
#include "array_list.h"
|
||||
#include "libgen.h"
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
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;
|
||||
bool mkdir_r(char *path) {
|
||||
char *path_duplicate = malloc(strlen(path) + 1);
|
||||
if (!path_duplicate) return false;
|
||||
strcpy(path_duplicate, path);
|
||||
char *path_current = (char *)malloc((strlen(path) + 2) * sizeof(char));
|
||||
if (!path_current) { free(path_duplicate); return false; }
|
||||
char *path_current_position = path_current;
|
||||
if (path[0] == '/') {
|
||||
path_current[0] = '/';
|
||||
path_current[1] = '\0';
|
||||
pos = 1;
|
||||
strcpy(path_current, "/");
|
||||
path_current_position += 1;
|
||||
} else {
|
||||
path_current[0] = '\0';
|
||||
}
|
||||
const char* delimiter = "/";
|
||||
char* saveptr;
|
||||
const char* part = strtok_r(path_duplicate, delimiter, &saveptr);
|
||||
const char *delimiter = "/";
|
||||
char *part = strtok(path_duplicate, delimiter);
|
||||
bool ok = true;
|
||||
while (part != NULL) {
|
||||
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';
|
||||
strcpy(path_current_position, part);
|
||||
path_current_position += strlen(part) * sizeof(char);
|
||||
strcpy(path_current_position, "/");
|
||||
path_current_position += sizeof(char);
|
||||
struct stat st;
|
||||
if (stat(path_current, &st) != 0) {
|
||||
if (mkdir(path_current, 0755) != 0) {
|
||||
@@ -53,40 +37,24 @@ bool mkdir_r(const char* path) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
part = strtok_r(NULL, delimiter, &saveptr);
|
||||
part = strtok(NULL, delimiter);
|
||||
}
|
||||
free(path_duplicate);
|
||||
free(path_current);
|
||||
return ok;
|
||||
}
|
||||
char* str_dup(const char* string) {
|
||||
|
||||
char *str_dup(const char *string) {
|
||||
if (string == NULL)
|
||||
return NULL;
|
||||
char* new_string = (char*)malloc(strlen(string) + 1);
|
||||
memcpy(new_string, string, strlen(string) + 1);
|
||||
char *new_string = (char *)malloc(strlen(string) + 1);
|
||||
strcpy(new_string, string);
|
||||
return new_string;
|
||||
}
|
||||
|
||||
bool glob_match(const char* pattern, const char* str) {
|
||||
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++;
|
||||
}
|
||||
return glob_match(pattern, str);
|
||||
}
|
||||
/* Single * — does not cross / boundaries */
|
||||
pattern++;
|
||||
while (*str && *str != '/') {
|
||||
if (glob_match(pattern, str))
|
||||
@@ -100,17 +68,8 @@ bool glob_match(const char* pattern, const char* str) {
|
||||
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);
|
||||
}
|
||||
if (*pattern != *str)
|
||||
return false;
|
||||
}
|
||||
pattern++;
|
||||
str++;
|
||||
}
|
||||
@@ -118,28 +77,17 @@ bool glob_match(const char* pattern, const char* str) {
|
||||
return *str == '\0';
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
const struct dirent* entry;
|
||||
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);
|
||||
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);
|
||||
@@ -148,16 +96,11 @@ static void delete_extras_walk(const char* abs_path, const char* rel_path, Array
|
||||
}
|
||||
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) {
|
||||
if (strcmp((char *)manifest->items[i], child_rel) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -165,44 +108,38 @@ static void delete_extras_walk(const char* abs_path, const char* rel_path, Array
|
||||
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);
|
||||
}
|
||||
rmdir(abs_path);
|
||||
}
|
||||
|
||||
void delete_extras(const char* dest_root, ArrayList* manifest) {
|
||||
void delete_extras(const char *dest_root, ArrayList *manifest) {
|
||||
delete_extras_walk(dest_root, "", manifest);
|
||||
}
|
||||
|
||||
char* path_cat(const char* path1, const char* path2) {
|
||||
char *path_cat(char *path1, char *path2) {
|
||||
if (path1 == NULL || *path1 == '\0')
|
||||
return str_dup(path2);
|
||||
if (path2 == NULL || *path2 == '\0')
|
||||
return str_dup(path1);
|
||||
int path1_len = strlen(path1);
|
||||
int path2_len = strlen(path2);
|
||||
char *path2_pointer = path2;
|
||||
if (path1[path1_len - 1] == '/')
|
||||
path1_len -= 1;
|
||||
if (path2[0] == '/') {
|
||||
path2++;
|
||||
path2_pointer += 1;
|
||||
path2_len -= 1;
|
||||
}
|
||||
char* new_path = malloc(path1_len + path2_len + 2);
|
||||
if (new_path == NULL)
|
||||
return NULL;
|
||||
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, path2_len);
|
||||
memcpy(new_path + path1_len + 1, path2_pointer, path2_len);
|
||||
new_path[path1_len + path2_len + 1] = '\0';
|
||||
return new_path;
|
||||
}
|
||||
|
||||
+5
-5
@@ -4,10 +4,10 @@
|
||||
#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, const char* path2);
|
||||
bool glob_match(const char* pattern, const char* str);
|
||||
void delete_extras(const char* dest_root, ArrayList* manifest);
|
||||
bool mkdir_r(char *path);
|
||||
char *str_dup(const char *string);
|
||||
char *path_cat(char *path1, char *path2);
|
||||
bool glob_match(const char *pattern, const char *str);
|
||||
void delete_extras(const char *dest_root, ArrayList *manifest);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -2,24 +2,10 @@
|
||||
#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>
|
||||
|
||||
@@ -39,20 +25,6 @@ int main() {
|
||||
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);
|
||||
|
||||
+11
-11
@@ -4,28 +4,28 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
static int destroyer_calls = 0;
|
||||
static void test_destroyer(void* item) {
|
||||
static void test_destroyer(void *item) {
|
||||
destroyer_calls++;
|
||||
free(item);
|
||||
}
|
||||
|
||||
void test_array_list() {
|
||||
ArrayList* list = array_list_create(free);
|
||||
ArrayList *list = array_list_create(free);
|
||||
EXPECT_NOT_NULL(list);
|
||||
EXPECT_EQ_INT(list->size, 0);
|
||||
EXPECT_EQ_INT(list->capacity, 100);
|
||||
|
||||
// Test adding
|
||||
int* val1 = malloc(sizeof(int));
|
||||
int *val1 = malloc(sizeof(int));
|
||||
*val1 = 42;
|
||||
array_list_add(list, val1);
|
||||
EXPECT_EQ_INT(list->size, 1);
|
||||
EXPECT_EQ_INT(*(int*)list->items[0], 42);
|
||||
EXPECT_EQ_INT(*(int *)list->items[0], 42);
|
||||
|
||||
// Test extending capacity
|
||||
// Initial capacity is 100. Let's add 105 elements.
|
||||
for (int i = 0; i < 105; i++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
int *val = malloc(sizeof(int));
|
||||
*val = i;
|
||||
array_list_add(list, val);
|
||||
}
|
||||
@@ -33,15 +33,15 @@ void test_array_list() {
|
||||
EXPECT_EQ_INT(list->capacity, 200); // 100 * 2
|
||||
|
||||
// Verify contents
|
||||
EXPECT_EQ_INT(*(int*)list->items[0], 42);
|
||||
EXPECT_EQ_INT(*(int*)list->items[1], 0);
|
||||
EXPECT_EQ_INT(*(int*)list->items[105], 104);
|
||||
EXPECT_EQ_INT(*(int *)list->items[0], 42);
|
||||
EXPECT_EQ_INT(*(int *)list->items[1], 0);
|
||||
EXPECT_EQ_INT(*(int *)list->items[105], 104);
|
||||
|
||||
// Test array conversion
|
||||
void** arr = array_list_to_array(list);
|
||||
void **arr = array_list_to_array(list);
|
||||
EXPECT_NOT_NULL(arr);
|
||||
EXPECT_EQ_INT(*(int*)arr[0], 42);
|
||||
EXPECT_EQ_INT(*(int*)arr[105], 104);
|
||||
EXPECT_EQ_INT(*(int *)arr[0], 42);
|
||||
EXPECT_EQ_INT(*(int *)arr[105], 104);
|
||||
free(arr);
|
||||
|
||||
// Delete list, verifying the destroyer is called 106 times
|
||||
|
||||
+13
-13
@@ -7,13 +7,13 @@
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_file_operations() {
|
||||
char* test_path = "temp_file_test.txt";
|
||||
char* test_content = "Hello, Chunk System!";
|
||||
char *test_path = "temp_file_test.txt";
|
||||
char *test_content = "Hello, Chunk System!";
|
||||
unsigned long long test_len = strlen(test_content);
|
||||
|
||||
to_disk(test_path, test_content, test_len);
|
||||
|
||||
File* f = file_create(test_path);
|
||||
File *f = file_create(test_path);
|
||||
EXPECT_NOT_NULL(f);
|
||||
EXPECT_EQ_STR(f->path, test_path);
|
||||
EXPECT_NOT_NULL(f->data);
|
||||
@@ -35,12 +35,12 @@ static void test_file_operations() {
|
||||
}
|
||||
|
||||
static void test_chunk_operations() {
|
||||
char* path1 = "temp_chunk_1.txt";
|
||||
char* content1 = "chunk item 1";
|
||||
char *path1 = "temp_chunk_1.txt";
|
||||
char *content1 = "chunk item 1";
|
||||
unsigned long long len1 = strlen(content1);
|
||||
|
||||
char* path2 = "temp_chunk_2.txt";
|
||||
char* content2 = "chunk item number 2";
|
||||
char *path2 = "temp_chunk_2.txt";
|
||||
char *content2 = "chunk item number 2";
|
||||
unsigned long long len2 = strlen(content2);
|
||||
|
||||
to_disk(path1, content1, len1);
|
||||
@@ -50,13 +50,13 @@ static void test_chunk_operations() {
|
||||
stat(path1, &st1);
|
||||
stat(path2, &st2);
|
||||
|
||||
File* f1 = file_create(path1);
|
||||
File *f1 = file_create(path1);
|
||||
f1->data->size = st1.st_size;
|
||||
File* f2 = file_create(path2);
|
||||
File *f2 = file_create(path2);
|
||||
f2->data->size = st2.st_size;
|
||||
|
||||
File* files[2] = {f1, f2};
|
||||
Chunk* chunk = chunk_create(files, 2);
|
||||
File *files[2] = {f1, f2};
|
||||
Chunk *chunk = chunk_create(files, 2);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 2);
|
||||
EXPECT_NOT_NULL(chunk->items[0]);
|
||||
@@ -67,10 +67,10 @@ static void test_chunk_operations() {
|
||||
file_load_data(f2);
|
||||
|
||||
// Test chunk_serialize / chunk_deserialize round-trip
|
||||
Data* serialized = chunk_serialize(chunk, false);
|
||||
Data *serialized = chunk_serialize(chunk, false);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
Chunk* deserialized = chunk_deserialize(serialized, false);
|
||||
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);
|
||||
|
||||
+20
-20
@@ -9,18 +9,18 @@
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_data_compress_decompress_roundtrip() {
|
||||
const char original[] = "Hello, World! This is test data for compression round-trip!";
|
||||
char original[] = "Hello, World! This is test data for compression round-trip!";
|
||||
size_t len = strlen(original);
|
||||
|
||||
char* buf = malloc(len);
|
||||
char *buf = malloc(len);
|
||||
memcpy(buf, original, len);
|
||||
Data* original_data = data_create(buf, len);
|
||||
Data *original_data = data_create(buf, len);
|
||||
EXPECT_NOT_NULL(original_data);
|
||||
|
||||
Data* compressed = data_compress(original_data, 3);
|
||||
Data *compressed = data_compress(original_data, 3);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed = data_decompress(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);
|
||||
@@ -32,18 +32,18 @@ static void test_data_compress_decompress_roundtrip() {
|
||||
|
||||
static void test_data_compress_decompress_large() {
|
||||
size_t size = 1024 * 10;
|
||||
char* original = malloc(size);
|
||||
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);
|
||||
Data *original_data = data_create(original, size);
|
||||
EXPECT_NOT_NULL(original_data);
|
||||
|
||||
Data* compressed = data_compress(original_data, 1);
|
||||
Data *compressed = data_compress(original_data, 1);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed = data_decompress(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);
|
||||
@@ -54,12 +54,12 @@ static void test_data_compress_decompress_large() {
|
||||
}
|
||||
|
||||
static void test_chunk_compress_decompress_roundtrip() {
|
||||
char* path1 = "temp_comp_test_1.txt";
|
||||
char* content1 = "chunk compression test file 1";
|
||||
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";
|
||||
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);
|
||||
@@ -69,9 +69,9 @@ static void test_chunk_compress_decompress_roundtrip() {
|
||||
EXPECT_EQ_INT(stat(path1, &st1), 0);
|
||||
EXPECT_EQ_INT(stat(path2, &st2), 0);
|
||||
|
||||
File* f1 = file_create(path1);
|
||||
File *f1 = file_create(path1);
|
||||
f1->data->size = st1.st_size;
|
||||
File* f2 = file_create(path2);
|
||||
File *f2 = file_create(path2);
|
||||
f2->data->size = st2.st_size;
|
||||
EXPECT_NOT_NULL(f1);
|
||||
EXPECT_NOT_NULL(f2);
|
||||
@@ -79,17 +79,17 @@ static void test_chunk_compress_decompress_roundtrip() {
|
||||
file_load_data(f1);
|
||||
file_load_data(f2);
|
||||
|
||||
File* files[2] = {f1, f2};
|
||||
Chunk* chunk = chunk_create(files, 2);
|
||||
File *files[2] = {f1, f2};
|
||||
Chunk *chunk = chunk_create(files, 2);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
|
||||
Data* compressed = chunk_compress(chunk, 3, false);
|
||||
Data *compressed = chunk_compress(chunk, 3, false);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed_data = data_decompress(compressed);
|
||||
Data *decompressed_data = data_decompress(compressed);
|
||||
EXPECT_NOT_NULL(decompressed_data);
|
||||
|
||||
Chunk* decompressed_chunk = chunk_deserialize(decompressed_data, false);
|
||||
Chunk *decompressed_chunk = chunk_deserialize(decompressed_data, false);
|
||||
EXPECT_NOT_NULL(decompressed_chunk);
|
||||
EXPECT_EQ_INT(decompressed_chunk->element_count, 2);
|
||||
|
||||
|
||||
+17
-17
@@ -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, false, 1, false, 0);
|
||||
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");
|
||||
@@ -23,8 +23,8 @@ static void test_config_lifecycle() {
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
@@ -38,8 +38,8 @@ static void test_config_ssh_dest() {
|
||||
}
|
||||
|
||||
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 *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);
|
||||
@@ -48,8 +48,8 @@ static void test_config_ssh_dest_local_path() {
|
||||
}
|
||||
|
||||
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 *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");
|
||||
@@ -58,12 +58,12 @@ static void test_config_ssh_dest_no_user() {
|
||||
}
|
||||
|
||||
static void test_pipeline_sender_lifecycle() {
|
||||
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);
|
||||
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);
|
||||
|
||||
PipelineContextSender* pcs = pipeline_context_sender_create(cfg, q1, q2);
|
||||
PipelineContextSender *pcs = pipeline_context_sender_create(cfg, q1, q2);
|
||||
EXPECT_NOT_NULL(pcs);
|
||||
EXPECT_EQ_STR(pcs->config->version, "2.0");
|
||||
EXPECT_EQ_INT(pcs->queue_scanner->capacity, 5);
|
||||
@@ -75,11 +75,11 @@ 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, false, 1, false, 0);
|
||||
Queue* q = queue_create(20, NULL);
|
||||
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);
|
||||
PipelineContextReceiver *pcr = pipeline_context_receiver_create(cfg, q, 42);
|
||||
EXPECT_NOT_NULL(pcr);
|
||||
EXPECT_EQ_STR(pcr->config->version, "3.0");
|
||||
EXPECT_EQ_INT(pcr->queue->capacity, 20);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_DATA_H
|
||||
#define TEST_DATA_H
|
||||
|
||||
void test_data();
|
||||
|
||||
#endif
|
||||
+40
-47
@@ -4,7 +4,7 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
static void test_adler32_basic() {
|
||||
const char* data = "Hello";
|
||||
const char *data = "Hello";
|
||||
uint32_t h = delta_adler32(data, 5);
|
||||
EXPECT_TRUE(h != 0);
|
||||
uint32_t h2 = delta_adler32(data, 5);
|
||||
@@ -12,15 +12,15 @@ static void test_adler32_basic() {
|
||||
}
|
||||
|
||||
static void test_adler32_different_data() {
|
||||
const char* a = "AAAA";
|
||||
const char* b = "BBBB";
|
||||
const char *a = "AAAA";
|
||||
const char *b = "BBBB";
|
||||
uint32_t ha = delta_adler32(a, 4);
|
||||
uint32_t hb = delta_adler32(b, 4);
|
||||
EXPECT_TRUE(ha != hb);
|
||||
}
|
||||
|
||||
static void test_xxhash32_basic() {
|
||||
const char* data = "Hello";
|
||||
const char *data = "Hello";
|
||||
uint32_t h = delta_xxhash32(data, 5);
|
||||
EXPECT_TRUE(h != 0);
|
||||
uint32_t h2 = delta_xxhash32(data, 5);
|
||||
@@ -28,8 +28,8 @@ static void test_xxhash32_basic() {
|
||||
}
|
||||
|
||||
static void test_xxhash32_different_data() {
|
||||
const char* a = "AAAA";
|
||||
const char* b = "BBBB";
|
||||
const char *a = "AAAA";
|
||||
const char *b = "BBBB";
|
||||
uint32_t ha = delta_xxhash32(a, 4);
|
||||
uint32_t hb = delta_xxhash32(b, 4);
|
||||
EXPECT_TRUE(ha != hb);
|
||||
@@ -37,18 +37,17 @@ static void test_xxhash32_different_data() {
|
||||
|
||||
static void test_signature_roundtrip() {
|
||||
char old_data[4096];
|
||||
for (int i = 0; i < 4096; i++)
|
||||
old_data[i] = (char)(i % 256);
|
||||
for (int i = 0; i < 4096; i++) old_data[i] = (char)(i % 256);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 4096, 1024);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
EXPECT_EQ_INT((int)sig->block_count, 4);
|
||||
EXPECT_EQ_INT((int)sig->block_size, 1024);
|
||||
|
||||
Data* serialized = delta_signature_serialize(sig);
|
||||
Data *serialized = delta_signature_serialize(sig);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
DeltaSignature* deserialized = delta_signature_deserialize(serialized);
|
||||
DeltaSignature *deserialized = delta_signature_deserialize(serialized);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT((int)deserialized->block_count, (int)sig->block_count);
|
||||
EXPECT_EQ_INT((int)deserialized->block_size, (int)sig->block_size);
|
||||
@@ -65,13 +64,12 @@ static void test_signature_roundtrip() {
|
||||
|
||||
static void test_delta_identical_files() {
|
||||
char data[2048];
|
||||
for (int i = 0; i < 2048; i++)
|
||||
data[i] = (char)(i % 128);
|
||||
for (int i = 0; i < 2048; i++) data[i] = (char)(i % 128);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(data, 2048, 512);
|
||||
DeltaSignature *sig = delta_signature_create(data, 2048, 512);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(data, 2048, sig, 512);
|
||||
Delta *delta = delta_compute(data, 2048, sig, 512);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
bool has_match = false;
|
||||
@@ -107,10 +105,10 @@ static void test_delta_small_edit() {
|
||||
new_data[101] = 'Y';
|
||||
new_data[102] = 'Z';
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 4096, 1024);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
Delta *delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
uint64_t total_literal = 0;
|
||||
@@ -124,7 +122,7 @@ static void test_delta_small_edit() {
|
||||
EXPECT_TRUE(match_count > 0);
|
||||
EXPECT_TRUE(total_literal < 4096);
|
||||
|
||||
void* reconstructed = delta_apply(old_data, 4096, delta, 1024);
|
||||
void *reconstructed = delta_apply(old_data, 4096, delta, 1024);
|
||||
EXPECT_NOT_NULL(reconstructed);
|
||||
EXPECT_EQ_INT(memcmp(reconstructed, new_data, 4096), 0);
|
||||
|
||||
@@ -141,10 +139,10 @@ static void test_delta_completely_different() {
|
||||
new_data[i] = (char)(i * 13 + 97);
|
||||
}
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 4096, 1024);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
Delta *delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
bool has_match = false;
|
||||
@@ -171,19 +169,19 @@ static void test_delta_serialize_roundtrip() {
|
||||
new_data[500] = 'A';
|
||||
new_data[501] = 'B';
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 4096, 1024);
|
||||
Delta *delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
Data* serialized = delta_serialize(delta);
|
||||
Data *serialized = delta_serialize(delta);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
Delta* deserialized = delta_deserialize(serialized);
|
||||
Delta *deserialized = delta_deserialize(serialized);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT((int)deserialized->new_file_size, (int)delta->new_file_size);
|
||||
EXPECT_EQ_INT((int)deserialized->instruction_count, (int)delta->instruction_count);
|
||||
|
||||
void* reconstructed = delta_apply(old_data, 4096, deserialized, 1024);
|
||||
void *reconstructed = delta_apply(old_data, 4096, deserialized, 1024);
|
||||
EXPECT_NOT_NULL(reconstructed);
|
||||
EXPECT_EQ_INT(memcmp(reconstructed, new_data, 4096), 0);
|
||||
|
||||
@@ -197,19 +195,17 @@ static void test_delta_serialize_roundtrip() {
|
||||
static void test_delta_file_growth() {
|
||||
char old_data[2048];
|
||||
char new_data[3072];
|
||||
for (int i = 0; i < 2048; i++)
|
||||
old_data[i] = (char)(i % 256);
|
||||
for (int i = 0; i < 2048; i++) old_data[i] = (char)(i % 256);
|
||||
memcpy(new_data, old_data, 2048);
|
||||
for (int i = 2048; i < 3072; i++)
|
||||
new_data[i] = (char)(i % 256);
|
||||
for (int i = 2048; i < 3072; i++) new_data[i] = (char)(i % 256);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 2048, 512);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 2048, 512);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(new_data, 3072, sig, 512);
|
||||
Delta *delta = delta_compute(new_data, 3072, sig, 512);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
void* reconstructed = delta_apply(old_data, 2048, delta, 512);
|
||||
void *reconstructed = delta_apply(old_data, 2048, delta, 512);
|
||||
EXPECT_NOT_NULL(reconstructed);
|
||||
EXPECT_EQ_INT(delta->new_file_size, 3072);
|
||||
EXPECT_EQ_INT(memcmp(reconstructed, new_data, 3072), 0);
|
||||
@@ -222,18 +218,16 @@ static void test_delta_file_growth() {
|
||||
static void test_delta_file_shrink() {
|
||||
char old_data[3072];
|
||||
char new_data[2048];
|
||||
for (int i = 0; i < 3072; i++)
|
||||
old_data[i] = (char)(i % 256);
|
||||
for (int i = 0; i < 2048; i++)
|
||||
new_data[i] = old_data[i];
|
||||
for (int i = 0; i < 3072; i++) old_data[i] = (char)(i % 256);
|
||||
for (int i = 0; i < 2048; i++) new_data[i] = old_data[i];
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 3072, 512);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, 3072, 512);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(new_data, 2048, sig, 512);
|
||||
Delta *delta = delta_compute(new_data, 2048, sig, 512);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
void* reconstructed = delta_apply(old_data, 3072, delta, 512);
|
||||
void *reconstructed = delta_apply(old_data, 3072, delta, 512);
|
||||
EXPECT_NOT_NULL(reconstructed);
|
||||
EXPECT_EQ_INT(delta->new_file_size, 2048);
|
||||
EXPECT_EQ_INT(memcmp(reconstructed, new_data, 2048), 0);
|
||||
@@ -279,24 +273,24 @@ static void test_large_file_delta() {
|
||||
uint64_t old_size = 200000;
|
||||
uint64_t new_size = 200000;
|
||||
|
||||
void* old_data = malloc((size_t)old_size);
|
||||
void* new_data = malloc((size_t)new_size);
|
||||
void *old_data = malloc((size_t)old_size);
|
||||
void *new_data = malloc((size_t)new_size);
|
||||
EXPECT_TRUE(old_data != NULL && new_data != NULL);
|
||||
|
||||
for (uint64_t i = 0; i < old_size; i++)
|
||||
((uint8_t*)old_data)[i] = (uint8_t)(i % 251);
|
||||
((uint8_t *)old_data)[i] = (uint8_t)(i % 251);
|
||||
memcpy(new_data, old_data, (size_t)old_size);
|
||||
|
||||
uint64_t offset = 100000;
|
||||
uint32_t change_len = 4096;
|
||||
for (uint32_t i = 0; i < change_len; i++)
|
||||
((uint8_t*)new_data)[offset + i] = (uint8_t)((i * 7 + 13) % 256);
|
||||
((uint8_t *)new_data)[offset + i] = (uint8_t)((i * 7 + 13) % 256);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, old_size, block_size);
|
||||
DeltaSignature *sig = delta_signature_create(old_data, old_size, block_size);
|
||||
EXPECT_TRUE(sig != NULL);
|
||||
EXPECT_TRUE(sig->block_count == (uint32_t)((old_size + block_size - 1) / block_size));
|
||||
|
||||
Delta* delta = delta_compute(new_data, new_size, sig, block_size);
|
||||
Delta *delta = delta_compute(new_data, new_size, sig, block_size);
|
||||
EXPECT_TRUE(delta != NULL);
|
||||
|
||||
uint64_t total_literal = 0;
|
||||
@@ -307,11 +301,10 @@ static void test_large_file_delta() {
|
||||
else
|
||||
match_count++;
|
||||
}
|
||||
EXPECT_TRUE(total_literal > 0);
|
||||
EXPECT_TRUE(match_count > 0);
|
||||
EXPECT_TRUE(delta->delta_size < new_size / 2);
|
||||
|
||||
void* result = delta_apply(old_data, old_size, delta, block_size);
|
||||
void *result = delta_apply(old_data, old_size, delta, block_size);
|
||||
EXPECT_TRUE(result != NULL);
|
||||
EXPECT_TRUE(delta->new_file_size == new_size);
|
||||
EXPECT_TRUE(memcmp(result, new_data, (size_t)new_size) == 0);
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
#include "test_file.h"
|
||||
#include "file.h"
|
||||
#include "data.h"
|
||||
#include "utils.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_file_create() {
|
||||
File* f = file_create("test_file_create.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
EXPECT_NOT_NULL(f->path);
|
||||
EXPECT_EQ_STR(f->path, "test_file_create.txt");
|
||||
EXPECT_NOT_NULL(f->data);
|
||||
EXPECT_NULL(f->data->data);
|
||||
EXPECT_EQ_INT((int)f->data->size, 0);
|
||||
EXPECT_NULL(f->metadata);
|
||||
file_destroy(f);
|
||||
}
|
||||
|
||||
static void test_file_destroy_null() {
|
||||
file_destroy(NULL);
|
||||
}
|
||||
|
||||
static void test_file_destroy_normal() {
|
||||
File* f = file_create("test_destroy.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
file_destroy(f);
|
||||
}
|
||||
|
||||
static void test_file_load_data() {
|
||||
const char* content = "Hello Load Test";
|
||||
EXPECT_TRUE(to_disk("test_file_load_data.txt", content, strlen(content)));
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_file_load_data.txt", &st), 0);
|
||||
|
||||
File* f = file_create("test_file_load_data.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
f->data->size = st.st_size;
|
||||
|
||||
EXPECT_TRUE(file_load_data(f));
|
||||
EXPECT_NOT_NULL(f->data->data);
|
||||
EXPECT_EQ_INT((int)f->data->size, (int)st.st_size);
|
||||
EXPECT_EQ_INT(memcmp(f->data->data, content, strlen(content)), 0);
|
||||
|
||||
file_destroy(f);
|
||||
unlink("test_file_load_data.txt");
|
||||
}
|
||||
|
||||
static void test_file_load_data_missing_file() {
|
||||
File* f = file_create("nonexistent_test_file_xyz.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
f->data->size = 10;
|
||||
EXPECT_FALSE(file_load_data(f));
|
||||
file_destroy(f);
|
||||
}
|
||||
|
||||
static void test_file_save_to_disk() {
|
||||
File* f = file_create("saved_file.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
const char* content = "Save to disk content";
|
||||
f->data->data = malloc(strlen(content));
|
||||
EXPECT_NOT_NULL(f->data->data);
|
||||
memcpy(f->data->data, content, strlen(content));
|
||||
f->data->size = strlen(content);
|
||||
|
||||
EXPECT_TRUE(file_save_to_disk("test_save_tmp", f));
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_save_tmp/saved_file.txt", &st), 0);
|
||||
|
||||
FILE* fp = fopen("test_save_tmp/saved_file.txt", "rb");
|
||||
EXPECT_NOT_NULL(fp);
|
||||
char buf[100];
|
||||
size_t nread = fread(buf, 1, sizeof(buf), fp);
|
||||
fclose(fp);
|
||||
EXPECT_EQ_INT((int)nread, (int)strlen(content));
|
||||
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
|
||||
|
||||
file_destroy(f);
|
||||
unlink("test_save_tmp/saved_file.txt");
|
||||
rmdir("test_save_tmp");
|
||||
}
|
||||
|
||||
static void test_to_disk_basic() {
|
||||
const char* content = "Basic to_disk test";
|
||||
EXPECT_TRUE(to_disk("test_to_disk_basic.txt", content, strlen(content)));
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_to_disk_basic.txt", &st), 0);
|
||||
EXPECT_EQ_INT((int)st.st_size, (int)strlen(content));
|
||||
|
||||
FILE* fp = fopen("test_to_disk_basic.txt", "rb");
|
||||
EXPECT_NOT_NULL(fp);
|
||||
char buf[100];
|
||||
size_t nread = fread(buf, 1, sizeof(buf), fp);
|
||||
fclose(fp);
|
||||
EXPECT_EQ_INT((int)nread, (int)strlen(content));
|
||||
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
|
||||
|
||||
unlink("test_to_disk_basic.txt");
|
||||
}
|
||||
|
||||
static void test_to_disk_creates_dirs() {
|
||||
const char* content = "Nested dir test";
|
||||
EXPECT_TRUE(to_disk("test_nested_tmp/nested/file.txt", content, strlen(content)));
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_nested_tmp/nested/file.txt", &st), 0);
|
||||
|
||||
FILE* fp = fopen("test_nested_tmp/nested/file.txt", "rb");
|
||||
EXPECT_NOT_NULL(fp);
|
||||
char buf[100];
|
||||
size_t nread = fread(buf, 1, sizeof(buf), fp);
|
||||
fclose(fp);
|
||||
EXPECT_EQ_INT((int)nread, (int)strlen(content));
|
||||
EXPECT_EQ_INT(memcmp(buf, content, strlen(content)), 0);
|
||||
|
||||
unlink("test_nested_tmp/nested/file.txt");
|
||||
rmdir("test_nested_tmp/nested");
|
||||
rmdir("test_nested_tmp");
|
||||
}
|
||||
|
||||
static void test_file_content_to_buffer() {
|
||||
const char* content = "Buffer content test";
|
||||
EXPECT_TRUE(to_disk("test_buffer_file.txt", content, strlen(content)));
|
||||
|
||||
File* f = file_create("test_buffer_file.txt");
|
||||
EXPECT_NOT_NULL(f);
|
||||
f->data->size = strlen(content);
|
||||
f->data->data = malloc(f->data->size);
|
||||
EXPECT_NOT_NULL(f->data->data);
|
||||
|
||||
size_t bytes_read = file_content_to_buffer(f);
|
||||
EXPECT_EQ_INT((int)bytes_read, (int)strlen(content));
|
||||
EXPECT_EQ_INT(memcmp(f->data->data, content, strlen(content)), 0);
|
||||
|
||||
file_destroy(f);
|
||||
unlink("test_buffer_file.txt");
|
||||
}
|
||||
|
||||
static void test_file_send_receive() {
|
||||
File* file = file_create("test_send_recv.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
const char* content = "Hello, File Send!";
|
||||
size_t len = strlen(content);
|
||||
file->data->data = malloc(len);
|
||||
EXPECT_NOT_NULL(file->data->data);
|
||||
memcpy(file->data->data, content, len);
|
||||
file->data->size = len;
|
||||
|
||||
Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
|
||||
false, false, false, false, 0, false, 0);
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
File* received = file_receive(cfg, p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else {
|
||||
if (!received->path || strcmp(received->path, "test_send_recv.txt") != 0)
|
||||
ok = false;
|
||||
if (!received->data || received->data->size != len)
|
||||
ok = false;
|
||||
else if (memcmp(received->data->data, content, len) != 0)
|
||||
ok = false;
|
||||
}
|
||||
|
||||
file_destroy(received);
|
||||
config_delete(cfg);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
bool sent = file_send_single_calls(file, p[1], false, 0, true);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
config_delete(cfg);
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_file_send_no_path() {
|
||||
File* file = file_create("test_no_path.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
const char* content = "No Path Data";
|
||||
size_t len = strlen(content);
|
||||
file->data->data = malloc(len);
|
||||
EXPECT_NOT_NULL(file->data->data);
|
||||
memcpy(file->data->data, content, len);
|
||||
file->data->size = len;
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
// Read file type indicator
|
||||
int file_type;
|
||||
EXPECT_TRUE(receive_int(p[0], &file_type));
|
||||
EXPECT_EQ_INT(file_type, (int)FILE_TYPE_REGULAR);
|
||||
Data* received = receive_data(p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else if (received->size != len)
|
||||
ok = false;
|
||||
else if (memcmp(received->data, content, len) != 0)
|
||||
ok = false;
|
||||
|
||||
data_destroy(received);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
bool sent = file_send_single_calls(file, p[1], false, 0, false);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_file_metadata_create() {
|
||||
EXPECT_TRUE(to_disk("test_meta_file.txt", "metadata test", 13));
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_meta_file.txt", &st), 0);
|
||||
|
||||
FileMetadata* m = file_metadata_create(&st);
|
||||
EXPECT_NOT_NULL(m);
|
||||
EXPECT_EQ_INT(m->mode, st.st_mode);
|
||||
EXPECT_EQ_INT(m->uid, st.st_uid);
|
||||
EXPECT_EQ_INT(m->gid, st.st_gid);
|
||||
EXPECT_EQ_INT((int)m->mtime_sec, (int)st.st_mtime);
|
||||
|
||||
file_metadata_destroy(m);
|
||||
unlink("test_meta_file.txt");
|
||||
}
|
||||
|
||||
void test_file() {
|
||||
test_file_create();
|
||||
test_file_destroy_null();
|
||||
test_file_destroy_normal();
|
||||
test_file_load_data();
|
||||
test_file_load_data_missing_file();
|
||||
test_file_save_to_disk();
|
||||
test_to_disk_basic();
|
||||
test_to_disk_creates_dirs();
|
||||
test_file_content_to_buffer();
|
||||
if (!is_running_under_valgrind()) {
|
||||
// Fork tests are skipped under valgrind because the parent process runs
|
||||
// orders of magnitude slower than the child (parent is instrumented, child
|
||||
// is not), which causes pipe-based protocol handshake timeouts. The parent
|
||||
// process itself has zero valgrind errors -- the failures are all in the
|
||||
// forked children where inherited allocations are reported as leaks.
|
||||
test_file_send_receive();
|
||||
test_file_send_no_path();
|
||||
}
|
||||
test_file_metadata_create();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_FILE_H
|
||||
#define TEST_FILE_H
|
||||
|
||||
void test_file();
|
||||
|
||||
#endif
|
||||
@@ -1,273 +0,0 @@
|
||||
#include "test_file_sendfile.h"
|
||||
#include "file.h"
|
||||
#include "config.h"
|
||||
#include "protocol.h"
|
||||
#include "utils.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Test basic sendfile transfer: create a file, send it via file_send_sendfile,
|
||||
* receive via file_receive, and verify contents. */
|
||||
static void test_sendfile_basic() {
|
||||
const char* content = "Hello from sendfile test!";
|
||||
size_t len = strlen(content);
|
||||
EXPECT_TRUE(to_disk("test_sendfile_basic.txt", content, len));
|
||||
|
||||
File* file = file_create("test_sendfile_basic.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
/* Set the size so file_send_sendfile can report it */
|
||||
file->data->size = len;
|
||||
|
||||
Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
|
||||
false, false, false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
/* Child: receive */
|
||||
close(p[1]);
|
||||
File* received = file_receive(cfg, p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else {
|
||||
if (!received->path || strcmp(received->path, "test_sendfile_basic.txt") != 0)
|
||||
ok = false;
|
||||
if (!received->data || received->data->size != len)
|
||||
ok = false;
|
||||
else if (memcmp(received->data->data, content, len) != 0)
|
||||
ok = false;
|
||||
}
|
||||
file_destroy(received);
|
||||
config_delete(cfg);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
/* Parent: send via sendfile */
|
||||
close(p[0]);
|
||||
bool sent = file_send_sendfile(file, p[1], false, 0, true);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
config_delete(cfg);
|
||||
unlink("test_sendfile_basic.txt");
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Test sendfile with an empty file */
|
||||
static void test_sendfile_empty_file() {
|
||||
const char* content = "";
|
||||
size_t len = 0;
|
||||
EXPECT_TRUE(to_disk("test_sendfile_empty.txt", content, len));
|
||||
|
||||
File* file = file_create("test_sendfile_empty.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
file->data->size = 0;
|
||||
|
||||
Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
|
||||
false, false, false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
File* received = file_receive(cfg, p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else {
|
||||
if (strcmp(received->path, "test_sendfile_empty.txt") != 0)
|
||||
ok = false;
|
||||
if (received->data->size != 0)
|
||||
ok = false;
|
||||
}
|
||||
file_destroy(received);
|
||||
config_delete(cfg);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
bool sent = file_send_sendfile(file, p[1], false, 0, true);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
config_delete(cfg);
|
||||
unlink("test_sendfile_empty.txt");
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Test error path: file does not exist on disk */
|
||||
static void test_sendfile_missing_file() {
|
||||
File* file = file_create("nonexistent_sendfile_test_file.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
file->data->size = 100; /* fake size */
|
||||
|
||||
/* Use a pipe that we can write to but sendfile should fail */
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
/* file_send_sendfile will try to open the nonexistent file -> should return false */
|
||||
bool sent = file_send_sendfile(file, p[1], false, 0, true);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
file_destroy(file);
|
||||
|
||||
EXPECT_FALSE(sent);
|
||||
}
|
||||
|
||||
/* Test compression level > 0 falls back to file_send_single_calls */
|
||||
static void test_sendfile_compression_fallback() {
|
||||
const char* content = "Compression fallback content";
|
||||
size_t len = strlen(content);
|
||||
EXPECT_TRUE(to_disk("test_sendfile_comp.txt", content, len));
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat("test_sendfile_comp.txt", &st), 0);
|
||||
|
||||
File* file = file_create("test_sendfile_comp.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
/* Load the file data into memory (required by file_send_single_calls fallback) */
|
||||
file->data->size = (size_t)st.st_size;
|
||||
EXPECT_TRUE(file_load_data(file));
|
||||
|
||||
Config* cfg = config_create(str_dup(PROTOCOL_VERSION), str_dup("/tmp"), str_dup("/tmp"), false,
|
||||
false, false, true, false, 3, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
File* received = file_receive(cfg, p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else {
|
||||
if (received->data->size != len)
|
||||
ok = false;
|
||||
else if (memcmp(received->data->data, content, len) != 0)
|
||||
ok = false;
|
||||
}
|
||||
file_destroy(received);
|
||||
config_delete(cfg);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
/* compression_level = 3 triggers fallback to file_send_single_calls */
|
||||
bool sent = file_send_sendfile(file, p[1], false, 3, true);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
config_delete(cfg);
|
||||
unlink("test_sendfile_comp.txt");
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Test sendfile without path (send_path = false) */
|
||||
static void test_sendfile_no_path() {
|
||||
const char* content = "No path sendfile test";
|
||||
size_t len = strlen(content);
|
||||
EXPECT_TRUE(to_disk("test_sendfile_nopath.txt", content, len));
|
||||
|
||||
File* file = file_create("test_sendfile_nopath.txt");
|
||||
EXPECT_NOT_NULL(file);
|
||||
file->data->size = len;
|
||||
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(p[1]);
|
||||
/* When send_path is false, the sender still sends file_type + data */
|
||||
int file_type;
|
||||
EXPECT_TRUE(receive_int(p[0], &file_type));
|
||||
EXPECT_EQ_INT(file_type, (int)FILE_TYPE_REGULAR);
|
||||
Data* received = receive_data(p[0]);
|
||||
close(p[0]);
|
||||
|
||||
bool ok = true;
|
||||
if (!received)
|
||||
ok = false;
|
||||
else if (received->size != len)
|
||||
ok = false;
|
||||
else if (memcmp(received->data, content, len) != 0)
|
||||
ok = false;
|
||||
|
||||
data_destroy(received);
|
||||
_exit(ok ? 0 : 1);
|
||||
} else {
|
||||
close(p[0]);
|
||||
bool sent = file_send_sendfile(file, p[1], false, 0, false);
|
||||
close(p[1]);
|
||||
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
file_destroy(file);
|
||||
unlink("test_sendfile_nopath.txt");
|
||||
|
||||
EXPECT_TRUE(sent);
|
||||
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
}
|
||||
|
||||
void test_file_sendfile() {
|
||||
if (!is_running_under_valgrind()) {
|
||||
// Fork tests are skipped under valgrind because the parent process runs
|
||||
// orders of magnitude slower than the child (parent is instrumented, child
|
||||
// is not), which causes pipe-based protocol handshake timeouts. The parent
|
||||
// process itself has zero valgrind errors -- the failures are all in the
|
||||
// forked children where inherited allocations are reported as leaks.
|
||||
test_sendfile_basic();
|
||||
test_sendfile_empty_file();
|
||||
test_sendfile_compression_fallback();
|
||||
test_sendfile_no_path();
|
||||
}
|
||||
test_sendfile_missing_file(); // no fork, safe under valgrind
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_FILE_SENDFILE_H
|
||||
#define TEST_FILE_SENDFILE_H
|
||||
|
||||
void test_file_sendfile();
|
||||
|
||||
#endif
|
||||
@@ -1,94 +0,0 @@
|
||||
#include "test_glob.h"
|
||||
#include "utils.h"
|
||||
#include "test_utils.h"
|
||||
#include <string.h>
|
||||
|
||||
static void test_glob_exact_match() {
|
||||
EXPECT_TRUE(glob_match("foo", "foo"));
|
||||
}
|
||||
|
||||
static void test_glob_question_mark() {
|
||||
EXPECT_TRUE(glob_match("f?o", "foo"));
|
||||
EXPECT_FALSE(glob_match("f?o", "fo"));
|
||||
}
|
||||
|
||||
static void test_glob_star() {
|
||||
EXPECT_TRUE(glob_match("*.txt", "foo.txt"));
|
||||
EXPECT_TRUE(glob_match("*.txt", "a.txt"));
|
||||
}
|
||||
|
||||
static void test_glob_star_mid() {
|
||||
EXPECT_TRUE(glob_match("f*o", "foo"));
|
||||
EXPECT_TRUE(glob_match("f*o", "fxxo"));
|
||||
EXPECT_FALSE(glob_match("f*o", "bar"));
|
||||
}
|
||||
|
||||
static void test_glob_no_match() {
|
||||
EXPECT_FALSE(glob_match("foo", "bar"));
|
||||
}
|
||||
|
||||
static void test_glob_empty_pattern() {
|
||||
EXPECT_TRUE(glob_match("", ""));
|
||||
EXPECT_FALSE(glob_match("", "foo"));
|
||||
}
|
||||
|
||||
static void test_glob_star_all() {
|
||||
EXPECT_TRUE(glob_match("*", "anything"));
|
||||
}
|
||||
|
||||
static void test_glob_slash_not_matched() {
|
||||
EXPECT_FALSE(glob_match("f*o", "f/o"));
|
||||
}
|
||||
|
||||
static void test_glob_complex() {
|
||||
EXPECT_TRUE(glob_match("*.c", "main.c"));
|
||||
EXPECT_FALSE(glob_match("*.c", "main.h"));
|
||||
}
|
||||
|
||||
static void test_glob_question_star() {
|
||||
EXPECT_TRUE(glob_match("?*.txt", "a.txt"));
|
||||
}
|
||||
|
||||
static void test_glob_doublestar_match_all() {
|
||||
EXPECT_TRUE(glob_match("**", "anything"));
|
||||
EXPECT_TRUE(glob_match("**", "path/to/file"));
|
||||
}
|
||||
|
||||
static void test_glob_doublestar_prefix() {
|
||||
EXPECT_TRUE(glob_match("**/foo", "foo"));
|
||||
EXPECT_TRUE(glob_match("**/foo", "bar/foo"));
|
||||
EXPECT_TRUE(glob_match("**/foo", "a/b/c/foo"));
|
||||
EXPECT_FALSE(glob_match("**/foo", "foobar"));
|
||||
EXPECT_FALSE(glob_match("**/foo", "bar/foobar"));
|
||||
}
|
||||
|
||||
static void test_glob_doublestar_suffix() {
|
||||
EXPECT_TRUE(glob_match("foo/**", "foo"));
|
||||
EXPECT_TRUE(glob_match("foo/**", "foo/bar"));
|
||||
EXPECT_TRUE(glob_match("foo/**", "foo/bar/baz"));
|
||||
EXPECT_FALSE(glob_match("foo/**", "foobar"));
|
||||
}
|
||||
|
||||
static void test_glob_doublestar_mid() {
|
||||
EXPECT_TRUE(glob_match("a/**/b", "a/b"));
|
||||
EXPECT_TRUE(glob_match("a/**/b", "a/x/b"));
|
||||
EXPECT_TRUE(glob_match("a/**/b", "a/x/y/z/b"));
|
||||
EXPECT_FALSE(glob_match("a/**/b", "a/x/bad"));
|
||||
}
|
||||
|
||||
void test_glob() {
|
||||
test_glob_exact_match();
|
||||
test_glob_question_mark();
|
||||
test_glob_star();
|
||||
test_glob_star_mid();
|
||||
test_glob_no_match();
|
||||
test_glob_empty_pattern();
|
||||
test_glob_star_all();
|
||||
test_glob_slash_not_matched();
|
||||
test_glob_complex();
|
||||
test_glob_question_star();
|
||||
test_glob_doublestar_match_all();
|
||||
test_glob_doublestar_prefix();
|
||||
test_glob_doublestar_suffix();
|
||||
test_glob_doublestar_mid();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_GLOB_H
|
||||
#define TEST_GLOB_H
|
||||
|
||||
void test_glob();
|
||||
|
||||
#endif
|
||||
@@ -1,116 +0,0 @@
|
||||
#include "test_log.h"
|
||||
#include "log.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
/* Test default log level: WARNING and ERROR should print, DEBUG and INFO should not.
|
||||
* We can't easily capture stderr in unit tests, so we verify the functions don't crash
|
||||
* and that set_log_level changes behavior. */
|
||||
|
||||
static void test_log_message_debug() {
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
log_message(LOG_LEVEL_DEBUG, "debug message: %d", 42);
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_message_info() {
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
log_message(LOG_LEVEL_INFO, "info message: %s", "test");
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_message_warning() {
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
log_message(LOG_LEVEL_WARNING, "warning message: %d %s", 1, "test");
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_message_error() {
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
log_message(LOG_LEVEL_ERROR, "error message: %s", "critical");
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_set_level_debug() {
|
||||
set_log_level(LOG_LEVEL_DEBUG);
|
||||
|
||||
/* After setting to DEBUG, all levels should be shown */
|
||||
log_message(LOG_LEVEL_DEBUG, "debug after set");
|
||||
log_message(LOG_LEVEL_INFO, "info after set");
|
||||
log_message(LOG_LEVEL_WARNING, "warning after set");
|
||||
log_message(LOG_LEVEL_ERROR, "error after set");
|
||||
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_set_level_info() {
|
||||
set_log_level(LOG_LEVEL_INFO);
|
||||
|
||||
/* INFO level should show INFO, WARNING, ERROR but not DEBUG */
|
||||
log_message(LOG_LEVEL_DEBUG, "debug should be filtered"); /* filtered */
|
||||
log_message(LOG_LEVEL_INFO, "info should show");
|
||||
log_message(LOG_LEVEL_WARNING, "warning should show");
|
||||
log_message(LOG_LEVEL_ERROR, "error should show");
|
||||
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
static void test_log_set_level_error() {
|
||||
set_log_level(LOG_LEVEL_ERROR);
|
||||
|
||||
/* ERROR level: only ERROR should show */
|
||||
log_message(LOG_LEVEL_DEBUG, "debug filtered");
|
||||
log_message(LOG_LEVEL_INFO, "info filtered");
|
||||
log_message(LOG_LEVEL_WARNING, "warning filtered");
|
||||
log_message(LOG_LEVEL_ERROR, "error should show");
|
||||
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
/* Test that set_log_level with default WARNING filters correctly */
|
||||
static void test_log_filtering() {
|
||||
/* Reset to default */
|
||||
set_log_level(LOG_LEVEL_WARNING);
|
||||
|
||||
/* These should be filtered */
|
||||
log_message(LOG_LEVEL_DEBUG, "filtered debug");
|
||||
log_message(LOG_LEVEL_INFO, "filtered info");
|
||||
|
||||
/* These should be shown */
|
||||
log_message(LOG_LEVEL_WARNING, "visible warning");
|
||||
log_message(LOG_LEVEL_ERROR, "visible error");
|
||||
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
/* Test that log_message handles various format strings */
|
||||
static void test_log_message_formats() {
|
||||
set_log_level(LOG_LEVEL_DEBUG);
|
||||
|
||||
log_message(LOG_LEVEL_DEBUG, "simple string");
|
||||
log_message(LOG_LEVEL_INFO, "integer: %d", -1);
|
||||
log_message(LOG_LEVEL_WARNING, "string: %s", "hello");
|
||||
log_message(LOG_LEVEL_ERROR, "multiple: %d %s %d", 1, "two", 3);
|
||||
|
||||
/* crash regression test — stderr capture would need infrastructure changes */
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
void test_log() {
|
||||
test_log_message_debug();
|
||||
test_log_message_info();
|
||||
test_log_message_warning();
|
||||
test_log_message_error();
|
||||
test_log_set_level_debug();
|
||||
test_log_set_level_info();
|
||||
test_log_set_level_error();
|
||||
test_log_filtering();
|
||||
test_log_message_formats();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_LOG_H
|
||||
#define TEST_LOG_H
|
||||
|
||||
void test_log();
|
||||
|
||||
#endif
|
||||
@@ -1,141 +0,0 @@
|
||||
#include "test_metadata.h"
|
||||
#include "metadata.h"
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_metadata_to_from_buf_roundtrip() {
|
||||
FileMetadata original;
|
||||
original.mode = 0755;
|
||||
original.uid = 1000;
|
||||
original.gid = 1000;
|
||||
original.mtime_sec = 1234567890;
|
||||
original.mtime_nsec = 500000000;
|
||||
|
||||
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, &original);
|
||||
|
||||
char* read_ptr = buf;
|
||||
FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
|
||||
EXPECT_NOT_NULL(result);
|
||||
EXPECT_EQ_INT(result->mode, 0755);
|
||||
EXPECT_EQ_INT(result->uid, 1000);
|
||||
EXPECT_EQ_INT(result->gid, 1000);
|
||||
EXPECT_EQ_INT(result->mtime_sec, 1234567890);
|
||||
EXPECT_EQ_INT(result->mtime_nsec, 500000000);
|
||||
|
||||
free(result);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_to_buf_null() {
|
||||
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, NULL);
|
||||
|
||||
char* read_ptr = buf;
|
||||
int present;
|
||||
memcpy(&present, read_ptr, sizeof(int));
|
||||
EXPECT_EQ_INT(present, 0);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_from_buf_null() {
|
||||
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
int present = 0;
|
||||
memcpy(buf, &present, sizeof(int));
|
||||
|
||||
char* read_ptr = buf;
|
||||
const FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
|
||||
EXPECT_NULL(result);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_send_receive_roundtrip() {
|
||||
io_set_bwlimit(0);
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
|
||||
FileMetadata original;
|
||||
original.mode = 0755;
|
||||
original.uid = 1000;
|
||||
original.gid = 1000;
|
||||
original.mtime_sec = 1234567890;
|
||||
original.mtime_nsec = 500000000;
|
||||
|
||||
EXPECT_TRUE(metadata_send(p[1], &original));
|
||||
|
||||
int ok = 0;
|
||||
FileMetadata* received = metadata_receive(p[0], &ok);
|
||||
EXPECT_NOT_NULL(received);
|
||||
EXPECT_EQ_INT(ok, 1);
|
||||
EXPECT_EQ_INT(received->mode, 0755);
|
||||
EXPECT_EQ_INT(received->uid, 1000);
|
||||
EXPECT_EQ_INT(received->gid, 1000);
|
||||
EXPECT_EQ_INT(received->mtime_sec, 1234567890);
|
||||
EXPECT_EQ_INT(received->mtime_nsec, 500000000);
|
||||
|
||||
free(received);
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_metadata_send_null() {
|
||||
io_set_bwlimit(0);
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
|
||||
EXPECT_TRUE(metadata_send(p[1], NULL));
|
||||
|
||||
int ok = 0;
|
||||
const FileMetadata* received = metadata_receive(p[0], &ok);
|
||||
EXPECT_NULL(received);
|
||||
EXPECT_EQ_INT(ok, 1);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_file_restore_metadata() {
|
||||
const char* path = "temp_meta_restore_test.txt";
|
||||
const char* content = "test content";
|
||||
EXPECT_TRUE(to_disk(path, content, strlen(content)));
|
||||
|
||||
FileMetadata m;
|
||||
m.mode = 0644;
|
||||
m.uid = getuid();
|
||||
m.gid = getgid();
|
||||
m.mtime_sec = 1234567890;
|
||||
m.mtime_nsec = 0;
|
||||
|
||||
file_restore_metadata(path, &m);
|
||||
|
||||
struct stat st;
|
||||
EXPECT_EQ_INT(stat(path, &st), 0);
|
||||
EXPECT_EQ_INT(st.st_mode & 07777, 0644);
|
||||
EXPECT_EQ_INT((int)st.st_mtime, 1234567890);
|
||||
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
void test_metadata() {
|
||||
test_metadata_to_from_buf_roundtrip();
|
||||
test_metadata_to_buf_null();
|
||||
test_metadata_from_buf_null();
|
||||
test_metadata_send_receive_roundtrip();
|
||||
test_metadata_send_null();
|
||||
test_file_restore_metadata();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_METADATA_H
|
||||
#define TEST_METADATA_H
|
||||
|
||||
void test_metadata();
|
||||
|
||||
#endif
|
||||
@@ -1,101 +0,0 @@
|
||||
#include "test_multiprocessing.h"
|
||||
#include "multiprocessing.h"
|
||||
#include "config.h"
|
||||
#include "queue.h"
|
||||
#include "utils.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
/* Test pipeline_context_sender_create/destroy with valid arguments */
|
||||
static void test_sender_create_destroy() {
|
||||
Config* cfg = config_create(str_dup("1.0"), str_dup("/src"), str_dup("/dst"), false, false, false,
|
||||
false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
Queue* q_scanner = queue_create(5, NULL);
|
||||
EXPECT_NOT_NULL(q_scanner);
|
||||
|
||||
Queue* q_loader = queue_create(10, NULL);
|
||||
EXPECT_NOT_NULL(q_loader);
|
||||
|
||||
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q_scanner, q_loader);
|
||||
EXPECT_NOT_NULL(ctx);
|
||||
EXPECT_EQ_STR(ctx->config->version, "1.0");
|
||||
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 5);
|
||||
EXPECT_EQ_INT(ctx->queue_loader->capacity, 10);
|
||||
EXPECT_FALSE(ctx->scanner_done);
|
||||
EXPECT_FALSE(ctx->loader_done);
|
||||
EXPECT_NULL(ctx->manifest);
|
||||
|
||||
pipeline_context_sender_destroy(ctx);
|
||||
}
|
||||
|
||||
/* Test pipeline_context_receiver_create/destroy with valid arguments */
|
||||
static void test_receiver_create_destroy() {
|
||||
Config* cfg = config_create(str_dup("2.0"), str_dup("/src"), str_dup("/dst"), true, true, false,
|
||||
false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
Queue* q = queue_create(20, NULL);
|
||||
EXPECT_NOT_NULL(q);
|
||||
|
||||
PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 42);
|
||||
EXPECT_NOT_NULL(ctx);
|
||||
EXPECT_EQ_STR(ctx->config->version, "2.0");
|
||||
EXPECT_EQ_INT(ctx->queue->capacity, 20);
|
||||
EXPECT_EQ_INT(ctx->file_descriptor, 42);
|
||||
EXPECT_FALSE(ctx->receiver_done);
|
||||
|
||||
pipeline_context_receiver_destroy(ctx);
|
||||
}
|
||||
|
||||
/* Test that create handles various queue capacities */
|
||||
static void test_sender_queue_capacities() {
|
||||
Config* cfg = config_create(str_dup("3.0"), str_dup("/src"), str_dup("/dst"), false, false, false,
|
||||
false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
/* Single-element queues */
|
||||
Queue* q1 = queue_create(1, NULL);
|
||||
Queue* q2 = queue_create(1, NULL);
|
||||
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2);
|
||||
EXPECT_NOT_NULL(ctx);
|
||||
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 1);
|
||||
EXPECT_EQ_INT(ctx->queue_loader->capacity, 1);
|
||||
pipeline_context_sender_destroy(ctx);
|
||||
}
|
||||
|
||||
/* Test that create handles zero-capacity queues */
|
||||
static void test_sender_zero_capacity() {
|
||||
Config* cfg = config_create(str_dup("4.0"), str_dup("/src"), str_dup("/dst"), false, false, false,
|
||||
false, false, 0, false, 0);
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
Queue* q1 = queue_create(0, NULL);
|
||||
Queue* q2 = queue_create(0, NULL);
|
||||
PipelineContextSender* ctx = pipeline_context_sender_create(cfg, q1, q2);
|
||||
EXPECT_NOT_NULL(ctx);
|
||||
EXPECT_EQ_INT(ctx->queue_scanner->capacity, 0);
|
||||
EXPECT_EQ_INT(ctx->queue_loader->capacity, 0);
|
||||
pipeline_context_sender_destroy(ctx);
|
||||
}
|
||||
|
||||
/* Test receiver with zero file_descriptor */
|
||||
static void test_receiver_fd_zero() {
|
||||
Config* cfg = config_create(str_dup("5.0"), str_dup("/src"), str_dup("/dst"), false, false, false,
|
||||
false, false, 0, false, 0);
|
||||
Queue* q = queue_create(5, NULL);
|
||||
PipelineContextReceiver* ctx = pipeline_context_receiver_create(cfg, q, 0);
|
||||
EXPECT_NOT_NULL(ctx);
|
||||
EXPECT_EQ_INT(ctx->file_descriptor, 0);
|
||||
EXPECT_FALSE(ctx->receiver_done);
|
||||
pipeline_context_receiver_destroy(ctx);
|
||||
}
|
||||
|
||||
void test_multiprocessing() {
|
||||
test_sender_create_destroy();
|
||||
test_receiver_create_destroy();
|
||||
test_sender_queue_capacities();
|
||||
test_sender_zero_capacity();
|
||||
test_receiver_fd_zero();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_MULTIPROCESSING_H
|
||||
#define TEST_MULTIPROCESSING_H
|
||||
|
||||
void test_multiprocessing();
|
||||
|
||||
#endif
|
||||
@@ -1,120 +0,0 @@
|
||||
#include "test_property.h"
|
||||
#include "test_utils.h"
|
||||
#include "chunk.h"
|
||||
#include "delta.h"
|
||||
#include "data.h"
|
||||
#include "compression.h"
|
||||
#include "file.h"
|
||||
#include "utils.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static Data* random_data(int min_size, int max_size) {
|
||||
int size = min_size + rand() % (max_size - min_size + 1);
|
||||
char* buf = malloc(size);
|
||||
for (int i = 0; i < size; i++)
|
||||
buf[i] = (char)(rand() % 256);
|
||||
return data_create(buf, size);
|
||||
}
|
||||
|
||||
static void test_property_compress_roundtrip() {
|
||||
for (int iter = 0; iter < 10; iter++) {
|
||||
Data* original = random_data(1, 10000);
|
||||
EXPECT_NOT_NULL(original);
|
||||
|
||||
size_t orig_size = original->size;
|
||||
void* orig_copy = malloc(orig_size);
|
||||
EXPECT_NOT_NULL(orig_copy);
|
||||
memcpy(orig_copy, original->data, orig_size);
|
||||
|
||||
Data* compressed = data_compress(original, 3);
|
||||
EXPECT_NOT_NULL(compressed);
|
||||
|
||||
Data* decompressed = data_decompress(compressed);
|
||||
EXPECT_NOT_NULL(decompressed);
|
||||
EXPECT_EQ_INT((int)decompressed->size, (int)orig_size);
|
||||
EXPECT_EQ_INT(memcmp(decompressed->data, orig_copy, orig_size), 0);
|
||||
|
||||
free(orig_copy);
|
||||
data_destroy(original);
|
||||
data_destroy(compressed);
|
||||
data_destroy(decompressed);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_property_delta_roundtrip() {
|
||||
for (int iter = 0; iter < 5; iter++) {
|
||||
char old_data[4096], new_data[4096];
|
||||
for (int i = 0; i < 4096; i++) {
|
||||
old_data[i] = (char)(rand() % 256);
|
||||
new_data[i] = old_data[i];
|
||||
}
|
||||
|
||||
int num_changes = 1 + rand() % 10;
|
||||
for (int c = 0; c < num_changes; c++) {
|
||||
int offset = rand() % 4096;
|
||||
new_data[offset] = (char)(rand() % 256);
|
||||
}
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
EXPECT_NOT_NULL(sig);
|
||||
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
EXPECT_NOT_NULL(delta);
|
||||
|
||||
void* result = delta_apply(old_data, 4096, delta, 1024);
|
||||
EXPECT_NOT_NULL(result);
|
||||
EXPECT_EQ_INT(memcmp(result, new_data, 4096), 0);
|
||||
|
||||
free(result);
|
||||
delta_signature_destroy(sig);
|
||||
delta_destroy(delta);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_property_chunk_roundtrip() {
|
||||
for (int iter = 0; iter < 5; iter++) {
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "test_prop_chunk_%d.txt", iter);
|
||||
|
||||
int content_len = 1 + rand() % 4096;
|
||||
char* content = malloc(content_len);
|
||||
for (int i = 0; i < content_len; i++)
|
||||
content[i] = (char)(rand() % 256);
|
||||
|
||||
to_disk(path, content, content_len);
|
||||
|
||||
struct stat st;
|
||||
stat(path, &st);
|
||||
|
||||
File* f = file_create(path);
|
||||
f->data->size = st.st_size;
|
||||
file_load_data(f);
|
||||
|
||||
File* files[1] = {f};
|
||||
Chunk* chunk = chunk_create(files, 1);
|
||||
Data* serialized = chunk_serialize(chunk, false);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
Chunk* deserialized = chunk_deserialize(serialized, false);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT(deserialized->element_count, 1);
|
||||
EXPECT_EQ_INT((int)deserialized->items[0]->data->size, content_len);
|
||||
EXPECT_EQ_INT(memcmp(deserialized->items[0]->data->data, content, content_len), 0);
|
||||
|
||||
free(content);
|
||||
data_destroy(serialized);
|
||||
chunk_destroy(deserialized);
|
||||
chunk_destroy(chunk);
|
||||
unlink(path);
|
||||
}
|
||||
}
|
||||
|
||||
void test_property() {
|
||||
srand(42);
|
||||
test_property_compress_roundtrip();
|
||||
test_property_delta_roundtrip();
|
||||
test_property_chunk_roundtrip();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_PROPERTY_H
|
||||
#define TEST_PROPERTY_H
|
||||
|
||||
void test_property();
|
||||
|
||||
#endif
|
||||
@@ -1,204 +0,0 @@
|
||||
#include "protocol.h"
|
||||
#include "test_utils.h"
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_send_receive_n_data() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
const char payload[] = "binary\x00test";
|
||||
size_t len = sizeof(payload);
|
||||
EXPECT_TRUE(send_n_data(0, payload, len));
|
||||
|
||||
char buf[64];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
EXPECT_TRUE(receive_n_data(0, buf, len));
|
||||
EXPECT_EQ_INT(memcmp(buf, payload, len), 0);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_n_data_zero() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
EXPECT_TRUE(send_n_data(0, "", 0));
|
||||
|
||||
char buf[4];
|
||||
EXPECT_TRUE(receive_n_data(0, buf, 0));
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_str() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
EXPECT_TRUE(send_str(0, ""));
|
||||
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
char* received = receive_str(0);
|
||||
EXPECT_NOT_NULL(received);
|
||||
EXPECT_EQ_STR(received, "");
|
||||
free(received);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_str_normal() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
EXPECT_TRUE(send_str(0, "Hello, Protocol!"));
|
||||
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
char* received = receive_str(0);
|
||||
EXPECT_NOT_NULL(received);
|
||||
EXPECT_EQ_STR(received, "Hello, Protocol!");
|
||||
free(received);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_data() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
unsigned char bin[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF};
|
||||
void* buf = malloc(sizeof(bin));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
memcpy(buf, bin, sizeof(bin));
|
||||
Data* original = data_create(buf, sizeof(bin));
|
||||
EXPECT_TRUE(send_data(0, original));
|
||||
|
||||
Data* received = receive_data(0);
|
||||
EXPECT_NOT_NULL(received);
|
||||
EXPECT_EQ_INT((int)received->size, (int)sizeof(bin));
|
||||
EXPECT_EQ_INT(memcmp(received->data, bin, sizeof(bin)), 0);
|
||||
|
||||
data_destroy(original);
|
||||
data_destroy(received);
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_int() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
int val = 42;
|
||||
EXPECT_TRUE(send_int(0, val));
|
||||
int received = 0;
|
||||
EXPECT_TRUE(receive_int(0, &received));
|
||||
EXPECT_EQ_INT(received, 42);
|
||||
|
||||
val = 0;
|
||||
EXPECT_TRUE(send_int(0, val));
|
||||
EXPECT_TRUE(receive_int(0, &received));
|
||||
EXPECT_EQ_INT(received, 0);
|
||||
|
||||
val = INT_MAX;
|
||||
EXPECT_TRUE(send_int(0, val));
|
||||
EXPECT_TRUE(receive_int(0, &received));
|
||||
EXPECT_EQ_INT(received, INT_MAX);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_send_receive_status() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Status statuses[] = {STATUS_OK, STATUS_ERROR, STATUS_FINISHED, STATUS_NEXT,
|
||||
STATUS_CHUNK, STATUS_CHECK, STATUS_DELTA_SIGNATURE, STATUS_DELTA_DATA};
|
||||
int count = sizeof(statuses) / sizeof(statuses[0]);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
EXPECT_TRUE(send_status(0, statuses[i]));
|
||||
Status received = -1;
|
||||
EXPECT_TRUE(receive_status(0, &received));
|
||||
EXPECT_EQ_INT((int)received, (int)statuses[i]);
|
||||
}
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
static void test_receive_n_data_truncated() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
close(p[1]);
|
||||
|
||||
char buf[32];
|
||||
EXPECT_FALSE(receive_n_data(0, buf, 32));
|
||||
|
||||
close(p[0]);
|
||||
}
|
||||
|
||||
static void test_receive_str_truncated() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
close(p[1]);
|
||||
|
||||
const char* received = receive_str(0);
|
||||
EXPECT_NULL(received);
|
||||
|
||||
close(p[0]);
|
||||
}
|
||||
|
||||
static void test_receive_str_oversized() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
|
||||
/* Send a size exceeding MAX_STRING_SIZE */
|
||||
size_t huge = MAX_STRING_SIZE + 1;
|
||||
EXPECT_TRUE(send_n_data(0, &huge, sizeof(size_t)));
|
||||
|
||||
const char* received = receive_str(0);
|
||||
EXPECT_NULL(received);
|
||||
|
||||
close(p[0]);
|
||||
close(p[1]);
|
||||
}
|
||||
|
||||
void test_protocol() {
|
||||
test_send_receive_n_data();
|
||||
test_send_receive_n_data_zero();
|
||||
test_send_receive_str();
|
||||
test_send_receive_str_normal();
|
||||
test_send_receive_data();
|
||||
test_send_receive_int();
|
||||
test_send_receive_status();
|
||||
test_receive_n_data_truncated();
|
||||
test_receive_str_truncated();
|
||||
test_receive_str_oversized();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_PROTOCOL_H
|
||||
#define TEST_PROTOCOL_H
|
||||
|
||||
void test_protocol();
|
||||
|
||||
#endif
|
||||
+36
-35
@@ -7,12 +7,12 @@
|
||||
#include <stdio.h>
|
||||
|
||||
static void test_queue_basic() {
|
||||
Queue* q = queue_create(10, NULL);
|
||||
Queue *q = queue_create(10, NULL);
|
||||
EXPECT_NOT_NULL(q);
|
||||
EXPECT_TRUE(queue_is_empty(q));
|
||||
EXPECT_FALSE(queue_is_full(q));
|
||||
|
||||
int* vals[5];
|
||||
int *vals[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
vals[i] = malloc(sizeof(int));
|
||||
*vals[i] = (i + 1) * 10;
|
||||
@@ -23,19 +23,19 @@ static void test_queue_basic() {
|
||||
EXPECT_FALSE(queue_is_full(q));
|
||||
EXPECT_EQ_INT(q->size, 5);
|
||||
|
||||
int* v1 = (int*)queue_dequeue(q);
|
||||
int *v1 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v1);
|
||||
EXPECT_EQ_INT(*v1, 10);
|
||||
free(v1);
|
||||
|
||||
int* v2 = (int*)queue_dequeue(q);
|
||||
int *v2 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v2);
|
||||
EXPECT_EQ_INT(*v2, 20);
|
||||
free(v2);
|
||||
|
||||
EXPECT_EQ_INT(q->size, 3);
|
||||
|
||||
int* vals2[3];
|
||||
int *vals2[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
vals2[i] = malloc(sizeof(int));
|
||||
*vals2[i] = (i + 6) * 10;
|
||||
@@ -44,9 +44,9 @@ static void test_queue_basic() {
|
||||
|
||||
EXPECT_EQ_INT(q->size, 6);
|
||||
|
||||
const int expected_vals[] = {30, 40, 50, 60, 70, 80};
|
||||
int expected_vals[] = {30, 40, 50, 60, 70, 80};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int* v = (int*)queue_dequeue(q);
|
||||
int *v = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v);
|
||||
EXPECT_EQ_INT(*v, expected_vals[i]);
|
||||
free(v);
|
||||
@@ -57,7 +57,7 @@ static void test_queue_basic() {
|
||||
}
|
||||
|
||||
static void test_queue_resize() {
|
||||
Queue* q = queue_create(3, NULL);
|
||||
Queue *q = queue_create(3, NULL);
|
||||
EXPECT_NOT_NULL(q);
|
||||
EXPECT_EQ_INT(q->capacity, 3);
|
||||
|
||||
@@ -69,34 +69,34 @@ static void test_queue_resize() {
|
||||
|
||||
EXPECT_TRUE(queue_is_full(q));
|
||||
|
||||
const int* v1 = (const int*)queue_dequeue(q);
|
||||
int *v1 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v1);
|
||||
EXPECT_EQ_INT(*v1, 1);
|
||||
|
||||
// Now front = 1, rear = 0, size = 2 (wrapped state)
|
||||
queue_enqueue(q, &d);
|
||||
queue_enqueue(q, &d);
|
||||
EXPECT_TRUE(queue_is_full(q));
|
||||
|
||||
// This enqueue triggers capacity doubling
|
||||
queue_enqueue(q, &e);
|
||||
queue_enqueue(q, &e);
|
||||
EXPECT_FALSE(queue_is_full(q));
|
||||
EXPECT_EQ_INT(q->capacity, 6);
|
||||
EXPECT_EQ_INT(q->size, 4);
|
||||
|
||||
// Dequeue all and check order: B, C, D, E
|
||||
const int* v2 = (const int*)queue_dequeue(q);
|
||||
int *v2 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v2);
|
||||
EXPECT_EQ_INT(*v2, 2);
|
||||
|
||||
const int* v3 = (const int*)queue_dequeue(q);
|
||||
int *v3 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v3);
|
||||
EXPECT_EQ_INT(*v3, 3);
|
||||
|
||||
const int* v4 = (const int*)queue_dequeue(q);
|
||||
int *v4 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v4);
|
||||
EXPECT_EQ_INT(*v4, 4);
|
||||
|
||||
const int* v5 = (const int*)queue_dequeue(q);
|
||||
int *v5 = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v5);
|
||||
EXPECT_EQ_INT(*v5, 5);
|
||||
|
||||
@@ -105,23 +105,23 @@ static void test_queue_resize() {
|
||||
}
|
||||
|
||||
static int destroyer_calls = 0;
|
||||
static void my_destroyer(void* item) {
|
||||
static void my_destroyer(void *item) {
|
||||
destroyer_calls++;
|
||||
free(item);
|
||||
}
|
||||
|
||||
static void test_queue_destroyer() {
|
||||
destroyer_calls = 0;
|
||||
Queue* q = queue_create(5, my_destroyer);
|
||||
Queue *q = queue_create(5, my_destroyer);
|
||||
EXPECT_NOT_NULL(q);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
int *val = malloc(sizeof(int));
|
||||
*val = i;
|
||||
queue_enqueue(q, val);
|
||||
}
|
||||
|
||||
int* v = (int*)queue_dequeue(q);
|
||||
int *v = (int *)queue_dequeue(q);
|
||||
EXPECT_NOT_NULL(v);
|
||||
EXPECT_EQ_INT(*v, 0);
|
||||
free(v);
|
||||
@@ -131,19 +131,18 @@ static void test_queue_destroyer() {
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
Queue* q;
|
||||
mtx_t* mutex;
|
||||
cnd_t* cnd_empty;
|
||||
cnd_t* cnd_full;
|
||||
Queue *q;
|
||||
mtx_t *mutex;
|
||||
cnd_t *cnd_empty;
|
||||
cnd_t *cnd_full;
|
||||
bool done;
|
||||
int sum;
|
||||
} ThreadContext;
|
||||
|
||||
static int consumer_func(void* arg) {
|
||||
ThreadContext* ctx = (ThreadContext*)arg;
|
||||
static int consumer_func(void *arg) {
|
||||
ThreadContext *ctx = (ThreadContext *)arg;
|
||||
while (true) {
|
||||
int* val = (int*)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full,
|
||||
&ctx->done);
|
||||
int *val = (int *)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full, &ctx->done);
|
||||
if (val == NULL) {
|
||||
break;
|
||||
}
|
||||
@@ -154,7 +153,7 @@ static int consumer_func(void* arg) {
|
||||
}
|
||||
|
||||
static void test_queue_multithreaded() {
|
||||
Queue* q = queue_create(2, NULL);
|
||||
Queue *q = queue_create(2, NULL);
|
||||
mtx_t mutex;
|
||||
cnd_t cnd_empty;
|
||||
cnd_t cnd_full;
|
||||
@@ -163,19 +162,21 @@ static void test_queue_multithreaded() {
|
||||
cnd_init(&cnd_empty);
|
||||
cnd_init(&cnd_full);
|
||||
|
||||
ThreadContext ctx = {.q = q,
|
||||
.mutex = &mutex,
|
||||
.cnd_empty = &cnd_empty,
|
||||
.cnd_full = &cnd_full,
|
||||
.done = false,
|
||||
.sum = 0};
|
||||
ThreadContext ctx = {
|
||||
.q = q,
|
||||
.mutex = &mutex,
|
||||
.cnd_empty = &cnd_empty,
|
||||
.cnd_full = &cnd_full,
|
||||
.done = false,
|
||||
.sum = 0
|
||||
};
|
||||
|
||||
thrd_t consumer;
|
||||
int res = thrd_create(&consumer, consumer_func, &ctx);
|
||||
EXPECT_EQ_INT(res, thrd_success);
|
||||
|
||||
for (int i = 1; i <= 100; i++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
int *val = malloc(sizeof(int));
|
||||
*val = i;
|
||||
queue_enqueue_multithreaded(q, val, &mutex, &cnd_empty, &cnd_full);
|
||||
}
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
#include "test_robustness.h"
|
||||
#include "test_utils.h"
|
||||
#include "chunk.h"
|
||||
#include "delta.h"
|
||||
#include "data.h"
|
||||
#include "file.h"
|
||||
#include "protocol.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void test_chunk_deserialize_truncated() {
|
||||
char* path = "test_rob_trunc.txt";
|
||||
char* content = "hello";
|
||||
to_disk(path, content, strlen(content));
|
||||
|
||||
struct stat st;
|
||||
stat(path, &st);
|
||||
|
||||
File* f = file_create(path);
|
||||
f->data->size = st.st_size;
|
||||
file_load_data(f);
|
||||
|
||||
File* files[1] = {f};
|
||||
Chunk* chunk = chunk_create(files, 1);
|
||||
Data* serialized = chunk_serialize(chunk, false);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
size_t orig_size = serialized->size;
|
||||
serialized->size = orig_size / 2;
|
||||
|
||||
const Chunk* result = chunk_deserialize(serialized, false);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
serialized->size = orig_size;
|
||||
data_destroy(serialized);
|
||||
chunk_destroy(chunk);
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
static void test_chunk_deserialize_empty() {
|
||||
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB};
|
||||
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
|
||||
EXPECT_NOT_NULL(d);
|
||||
memcpy(d->data, garbage, sizeof(garbage));
|
||||
|
||||
const Chunk* result = chunk_deserialize(d, false);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_chunk_deserialize_garbage() {
|
||||
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA};
|
||||
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
|
||||
EXPECT_NOT_NULL(d);
|
||||
memcpy(d->data, garbage, sizeof(garbage));
|
||||
|
||||
const Chunk* result = chunk_deserialize(d, false);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_delta_deserialize_truncated() {
|
||||
char old_data[4096], new_data[4096];
|
||||
for (int i = 0; i < 4096; i++) {
|
||||
old_data[i] = (char)(i % 256);
|
||||
new_data[i] = old_data[i];
|
||||
}
|
||||
new_data[100] = 'X';
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
Data* serialized = delta_serialize(delta);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
serialized->size = 4;
|
||||
const Delta* result = delta_deserialize(serialized);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(serialized);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
}
|
||||
|
||||
static void test_delta_deserialize_empty() {
|
||||
char garbage[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
|
||||
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
|
||||
EXPECT_NOT_NULL(d);
|
||||
memcpy(d->data, garbage, sizeof(garbage));
|
||||
|
||||
const Delta* result = delta_deserialize(d);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_delta_deserialize_garbage() {
|
||||
unsigned char garbage[] = {0xFF, 0xFE, 0xFD, 0xFC, 0xFB, 0xFA};
|
||||
Data* d = data_create(malloc(sizeof(garbage)), sizeof(garbage));
|
||||
EXPECT_NOT_NULL(d);
|
||||
memcpy(d->data, garbage, sizeof(garbage));
|
||||
|
||||
const Delta* result = delta_deserialize(d);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(d);
|
||||
}
|
||||
|
||||
static void test_delta_signature_deserialize_truncated() {
|
||||
char old_data[4096];
|
||||
for (int i = 0; i < 4096; i++)
|
||||
old_data[i] = (char)(i % 256);
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
Data* serialized = delta_signature_serialize(sig);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
serialized->size = 4;
|
||||
const DeltaSignature* result = delta_signature_deserialize(serialized);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(serialized);
|
||||
delta_signature_destroy(sig);
|
||||
}
|
||||
|
||||
static void test_delta_deserialize_truncated_instructions() {
|
||||
// Create a real delta with 2 LITERAL instructions, serialize, then
|
||||
// truncate after the header so the instruction-loop error paths are
|
||||
// exercised (earlier tests with tiny buffers die at the 12-byte
|
||||
// header guard and never reach the instruction decoder).
|
||||
char old_data[4096], new_data[4096];
|
||||
for (int i = 0; i < 4096; i++) {
|
||||
old_data[i] = (char)(i % 256);
|
||||
new_data[i] = old_data[i];
|
||||
}
|
||||
// Two small changes to produce 2 LITERAL instructions
|
||||
new_data[100] = 'X';
|
||||
new_data[200] = 'Y';
|
||||
|
||||
DeltaSignature* sig = delta_signature_create(old_data, 4096, 1024);
|
||||
Delta* delta = delta_compute(new_data, 4096, sig, 1024);
|
||||
Data* serialized = delta_serialize(delta);
|
||||
EXPECT_NOT_NULL(serialized);
|
||||
|
||||
// Truncate to include the header (12 bytes) + partial first instruction
|
||||
serialized->size = 14;
|
||||
const Delta* result = delta_deserialize(serialized);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
data_destroy(serialized);
|
||||
delta_destroy(delta);
|
||||
delta_signature_destroy(sig);
|
||||
}
|
||||
|
||||
static void test_delta_apply_null() {
|
||||
const void* result = delta_apply(NULL, 0, NULL, 0);
|
||||
EXPECT_NULL(result);
|
||||
}
|
||||
|
||||
static void test_protocol_receive_n_data_closed_pipe() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
close(p[1]);
|
||||
|
||||
char buf[32];
|
||||
EXPECT_FALSE(receive_n_data(0, buf, 32));
|
||||
|
||||
close(p[0]);
|
||||
}
|
||||
|
||||
static void test_receive_data_closed_pipe() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
close(p[1]);
|
||||
|
||||
const Data* result = receive_data(0);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
close(p[0]);
|
||||
}
|
||||
|
||||
static void test_receive_str_closed_pipe() {
|
||||
int p[2];
|
||||
EXPECT_EQ_INT(pipe(p), 0);
|
||||
io_set_fds(p[0], p[1]);
|
||||
io_set_bwlimit(0);
|
||||
close(p[1]);
|
||||
|
||||
const char* result = receive_str(0);
|
||||
EXPECT_NULL(result);
|
||||
|
||||
close(p[0]);
|
||||
}
|
||||
|
||||
void test_robustness() {
|
||||
test_chunk_deserialize_truncated();
|
||||
test_chunk_deserialize_empty();
|
||||
test_chunk_deserialize_garbage();
|
||||
test_delta_deserialize_truncated();
|
||||
test_delta_deserialize_empty();
|
||||
test_delta_deserialize_garbage();
|
||||
test_delta_deserialize_truncated_instructions();
|
||||
test_delta_signature_deserialize_truncated();
|
||||
test_delta_apply_null();
|
||||
test_protocol_receive_n_data_closed_pipe();
|
||||
test_receive_data_closed_pipe();
|
||||
test_receive_str_closed_pipe();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_ROBUSTNESS_H
|
||||
#define TEST_ROBUSTNESS_H
|
||||
|
||||
void test_robustness();
|
||||
|
||||
#endif
|
||||
+33
-311
@@ -6,28 +6,27 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void create_test_file(const char* path, const char* content) {
|
||||
static void create_test_file(const char *path, const char *content) {
|
||||
(void)to_disk(path, content, strlen(content));
|
||||
}
|
||||
|
||||
static void test_scanner_single_file() {
|
||||
const char* dir = "test_scan_dir_single";
|
||||
const char* file1 = "test_scan_dir_single/file1.txt";
|
||||
const char* content1 = "hello scanner";
|
||||
const char *dir = "test_scan_dir_single";
|
||||
const char *file1 = "test_scan_dir_single/file1.txt";
|
||||
const char *content1 = "hello scanner";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
mkdir(dir, 0755);
|
||||
create_test_file(file1, content1);
|
||||
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
DirectoryScanner *scanner = directory_scanner_create((char *)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
Chunk *chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||
EXPECT_EQ_STR(chunk->items[0]->path, file1);
|
||||
|
||||
const Chunk* next = directory_scanner_next(scanner);
|
||||
Chunk *next = directory_scanner_next(scanner);
|
||||
EXPECT_NULL(next);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
@@ -37,38 +36,35 @@ static void test_scanner_single_file() {
|
||||
}
|
||||
|
||||
static void test_scanner_multiple_files() {
|
||||
const char* dir = "test_scan_dir_multi";
|
||||
const char* file1 = "test_scan_dir_multi/a.txt";
|
||||
const char* file2 = "test_scan_dir_multi/b.txt";
|
||||
const char* content1 = "alpha";
|
||||
const char* content2 = "beta";
|
||||
const char *dir = "test_scan_dir_multi";
|
||||
const char *file1 = "test_scan_dir_multi/a.txt";
|
||||
const char *file2 = "test_scan_dir_multi/b.txt";
|
||||
const char *content1 = "alpha";
|
||||
const char *content2 = "beta";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
mkdir(dir, 0755);
|
||||
create_test_file(file1, content1);
|
||||
create_test_file(file2, content2);
|
||||
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
DirectoryScanner *scanner = directory_scanner_create((char *)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
const Chunk* chunk = directory_scanner_next(scanner);
|
||||
Chunk *chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 2);
|
||||
|
||||
int found1 = 0, found2 = 0;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (strcmp(chunk->items[i]->path, file1) == 0)
|
||||
found1 = 1;
|
||||
if (strcmp(chunk->items[i]->path, file2) == 0)
|
||||
found2 = 1;
|
||||
if (strcmp(chunk->items[i]->path, file1) == 0) found1 = 1;
|
||||
if (strcmp(chunk->items[i]->path, file2) == 0) found2 = 1;
|
||||
}
|
||||
EXPECT_TRUE(found1);
|
||||
EXPECT_TRUE(found2);
|
||||
|
||||
const Chunk* next = directory_scanner_next(scanner);
|
||||
Chunk *next = directory_scanner_next(scanner);
|
||||
EXPECT_NULL(next);
|
||||
|
||||
chunk_destroy((void*)chunk);
|
||||
chunk_destroy(chunk);
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(file1);
|
||||
unlink(file2);
|
||||
@@ -76,23 +72,22 @@ static void test_scanner_multiple_files() {
|
||||
}
|
||||
|
||||
static void test_scanner_subdirectory() {
|
||||
const char* root = "test_scan_sub";
|
||||
const char* sub = "test_scan_sub/sub";
|
||||
const char* root_file = "test_scan_sub/root.txt";
|
||||
const char* sub_file = "test_scan_sub/sub/sub_file.txt";
|
||||
const char* content = "nested content";
|
||||
const char *root = "test_scan_sub";
|
||||
const char *sub = "test_scan_sub/sub";
|
||||
const char *root_file = "test_scan_sub/root.txt";
|
||||
const char *sub_file = "test_scan_sub/sub/sub_file.txt";
|
||||
const char *content = "nested content";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
||||
EXPECT_EQ_INT(mkdir(sub, 0755), 0);
|
||||
mkdir(root, 0755);
|
||||
mkdir(sub, 0755);
|
||||
create_test_file(root_file, content);
|
||||
create_test_file(sub_file, content);
|
||||
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)root, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
DirectoryScanner *scanner = directory_scanner_create((char *)root, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
int total_files = 0;
|
||||
Chunk* chunk;
|
||||
Chunk *chunk;
|
||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
total_files += chunk->element_count;
|
||||
chunk_destroy(chunk);
|
||||
@@ -107,296 +102,23 @@ static void test_scanner_subdirectory() {
|
||||
}
|
||||
|
||||
static void test_scanner_empty_directory() {
|
||||
const char* dir = "test_scan_empty";
|
||||
const char *dir = "test_scan_empty";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
mkdir(dir, 0755);
|
||||
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
DirectoryScanner *scanner = directory_scanner_create((char *)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
const Chunk* chunk = directory_scanner_next(scanner);
|
||||
Chunk *chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NULL(chunk);
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
/* --- Exclude/include pattern and size filter edge cases (Issue #56) --- */
|
||||
|
||||
static void test_scanner_exclude_pattern() {
|
||||
const char* dir = "test_scan_excl";
|
||||
const char* f_txt = "test_scan_excl/keep.txt";
|
||||
const char* f_tmp = "test_scan_excl/remove.tmp";
|
||||
const char* content = "data";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(f_txt, content);
|
||||
create_test_file(f_tmp, content);
|
||||
|
||||
char* exclude[] = {"*.tmp"};
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, exclude, 1, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||
EXPECT_EQ_STR(chunk->items[0]->path, f_txt);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(f_txt);
|
||||
unlink(f_tmp);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_exclude_subdirectory() {
|
||||
const char* root = "test_scan_excl_sub";
|
||||
const char* sub = "test_scan_excl_sub/sub";
|
||||
const char* root_txt = "test_scan_excl_sub/root.txt";
|
||||
const char* sub_txt = "test_scan_excl_sub/sub/data.txt";
|
||||
const char* sub_tmp = "test_scan_excl_sub/sub/temp.tmp";
|
||||
const char* content = "data";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(root, 0755), 0);
|
||||
EXPECT_EQ_INT(mkdir(sub, 0755), 0);
|
||||
create_test_file(root_txt, content);
|
||||
create_test_file(sub_txt, content);
|
||||
create_test_file(sub_tmp, content);
|
||||
|
||||
char* exclude[] = {"*.tmp"};
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)root, false, 0, exclude, 1, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
int total = 0;
|
||||
Chunk* chunk;
|
||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
total += chunk->element_count;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
size_t len = strlen(chunk->items[i]->path);
|
||||
EXPECT_TRUE(len < 4 || strcmp(chunk->items[i]->path + len - 4, ".tmp") != 0);
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
}
|
||||
EXPECT_EQ_INT(total, 2);
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(root_txt);
|
||||
unlink(sub_txt);
|
||||
unlink(sub_tmp);
|
||||
rmdir(sub);
|
||||
rmdir(root);
|
||||
}
|
||||
|
||||
static void test_scanner_include_and_exclude() {
|
||||
const char* dir = "test_scan_inc_exc";
|
||||
const char* f_txt = "test_scan_inc_exc/a.txt";
|
||||
const char* f_log = "test_scan_inc_exc/b.log";
|
||||
const char* f_bak = "test_scan_inc_exc/c.bak";
|
||||
const char* content = "filter";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(f_txt, content);
|
||||
create_test_file(f_log, content);
|
||||
create_test_file(f_bak, content);
|
||||
|
||||
char* exclude[] = {"*.bak"};
|
||||
char* include[] = {"*.txt", "*.log"};
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 2, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 2);
|
||||
|
||||
int found_txt = 0, found_log = 0;
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
if (strstr(chunk->items[i]->path, "a.txt"))
|
||||
found_txt = 1;
|
||||
if (strstr(chunk->items[i]->path, "b.log"))
|
||||
found_log = 1;
|
||||
}
|
||||
EXPECT_TRUE(found_txt);
|
||||
EXPECT_TRUE(found_log);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(f_txt);
|
||||
unlink(f_log);
|
||||
unlink(f_bak);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_max_size() {
|
||||
const char* dir = "test_scan_max";
|
||||
const char* small = "test_scan_max/small.txt";
|
||||
const char* large = "test_scan_max/large.txt";
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(small, "tiny");
|
||||
create_test_file(large, "this_content_is_longer_than_ten_chars");
|
||||
|
||||
/* max_size = 10 — only files <= 10 bytes */
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 10, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||
EXPECT_EQ_STR(chunk->items[0]->path, small);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(small);
|
||||
unlink(large);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_min_size() {
|
||||
const char* dir = "test_scan_min";
|
||||
const char* empty_f = "test_scan_min/empty.txt";
|
||||
const char* data_f = "test_scan_min/data.txt";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(empty_f, "");
|
||||
create_test_file(data_f, "some content here");
|
||||
|
||||
/* min_size = 1 — only files >= 1 byte */
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 1);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||
EXPECT_EQ_STR(chunk->items[0]->path, data_f);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(empty_f);
|
||||
unlink(data_f);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_size_range() {
|
||||
const char* dir = "test_scan_range";
|
||||
const char* tiny = "test_scan_range/tiny.txt";
|
||||
const char* medium = "test_scan_range/med.txt";
|
||||
const char* huge = "test_scan_range/huge.txt";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(tiny, "ab");
|
||||
create_test_file(medium, "hello world");
|
||||
create_test_file(huge, "this is a much larger file for testing size filters");
|
||||
|
||||
/* Only files between 3 and 20 bytes */
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 20, 3);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 1);
|
||||
EXPECT_EQ_STR(chunk->items[0]->path, medium);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(tiny);
|
||||
unlink(medium);
|
||||
unlink(huge);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_mixed_patterns() {
|
||||
/* Combine exclude, include, and size filters together */
|
||||
const char* dir = "test_scan_mixed";
|
||||
const char* a_txt = "test_scan_mixed/a.txt"; /* size ~= 5 */
|
||||
const char* b_bin = "test_scan_mixed/b.bin"; /* size ~= 13 */
|
||||
const char* c_txt = "test_scan_mixed/c.txt"; /* size ~= 5 */
|
||||
const char* d_bak = "test_scan_mixed/d.bak"; /* size ~= 42 */
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(a_txt, "aaaaa");
|
||||
create_test_file(b_bin, "bbbbbbbbbbbbb");
|
||||
create_test_file(c_txt, "ccccc");
|
||||
create_test_file(d_bak, "dddddddddddddddddddddddddddddddddddddddddd");
|
||||
|
||||
/* Exclude *.bak, include *.txt, min_size=3, max_size=10 */
|
||||
char* exclude[] = {"*.bak"};
|
||||
char* include[] = {"*.txt"};
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, exclude, 1, include, 1, 10, 3);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
/* Both a.txt and c.txt meet the criteria: .txt extension, size 5 <= 10 and >= 3 */
|
||||
EXPECT_EQ_INT(chunk->element_count, 2);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(a_txt);
|
||||
unlink(b_bin);
|
||||
unlink(c_txt);
|
||||
unlink(d_bak);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
static void test_scanner_no_patterns() {
|
||||
/* Explicit test with no exclude/include patterns and no size filters.
|
||||
* This verifies that NULL/0 for all pattern parameters works correctly. */
|
||||
const char* dir = "test_scan_none";
|
||||
const char* f1 = "test_scan_none/f1.txt";
|
||||
const char* f2 = "test_scan_none/f2.txt";
|
||||
|
||||
EXPECT_EQ_INT(mkdir(dir, 0755), 0);
|
||||
create_test_file(f1, "first");
|
||||
create_test_file(f2, "second");
|
||||
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create((char*)dir, false, 0, NULL, 0, NULL, 0, 0, 0);
|
||||
EXPECT_NOT_NULL(scanner);
|
||||
|
||||
Chunk* chunk = directory_scanner_next(scanner);
|
||||
EXPECT_NOT_NULL(chunk);
|
||||
EXPECT_EQ_INT(chunk->element_count, 2);
|
||||
|
||||
chunk_destroy(chunk);
|
||||
EXPECT_NULL(directory_scanner_next(scanner));
|
||||
|
||||
directory_scanner_destroy(scanner);
|
||||
unlink(f1);
|
||||
unlink(f2);
|
||||
rmdir(dir);
|
||||
}
|
||||
|
||||
void test_scanner() {
|
||||
test_scanner_single_file();
|
||||
test_scanner_multiple_files();
|
||||
test_scanner_subdirectory();
|
||||
test_scanner_empty_directory();
|
||||
/* Issue #56: scanner pattern edge cases */
|
||||
test_scanner_exclude_pattern();
|
||||
test_scanner_exclude_subdirectory();
|
||||
test_scanner_include_and_exclude();
|
||||
test_scanner_max_size();
|
||||
test_scanner_min_size();
|
||||
test_scanner_size_range();
|
||||
test_scanner_mixed_patterns();
|
||||
test_scanner_no_patterns();
|
||||
}
|
||||
|
||||
+21
-21
@@ -6,56 +6,56 @@
|
||||
|
||||
void test_shared_utils() {
|
||||
// Test str_dup
|
||||
const char* dup_null = str_dup(NULL);
|
||||
char *dup_null = str_dup(NULL);
|
||||
EXPECT_NULL(dup_null);
|
||||
|
||||
char* dup_empty = str_dup("");
|
||||
|
||||
char *dup_empty = str_dup("");
|
||||
EXPECT_NOT_NULL(dup_empty);
|
||||
EXPECT_EQ_STR(dup_empty, "");
|
||||
free(dup_empty);
|
||||
|
||||
char* dup_normal = str_dup("hello world");
|
||||
|
||||
char *dup_normal = str_dup("hello world");
|
||||
EXPECT_NOT_NULL(dup_normal);
|
||||
EXPECT_EQ_STR(dup_normal, "hello world");
|
||||
free(dup_normal);
|
||||
|
||||
|
||||
// Test path_cat
|
||||
char* cat1 = path_cat("/foo", "/bar");
|
||||
char *cat1 = path_cat("/foo", "/bar");
|
||||
EXPECT_NOT_NULL(cat1);
|
||||
EXPECT_EQ_STR(cat1, "/foo/bar");
|
||||
free(cat1);
|
||||
|
||||
char* cat2 = path_cat("/foo/", "/bar");
|
||||
|
||||
char *cat2 = path_cat("/foo/", "/bar");
|
||||
EXPECT_NOT_NULL(cat2);
|
||||
EXPECT_EQ_STR(cat2, "/foo/bar");
|
||||
free(cat2);
|
||||
|
||||
char* cat3 = path_cat("/foo", "bar");
|
||||
|
||||
char *cat3 = path_cat("/foo", "bar");
|
||||
EXPECT_NOT_NULL(cat3);
|
||||
EXPECT_EQ_STR(cat3, "/foo/bar");
|
||||
free(cat3);
|
||||
|
||||
char* cat4 = path_cat("/foo/", "bar");
|
||||
|
||||
char *cat4 = path_cat("/foo/", "bar");
|
||||
EXPECT_NOT_NULL(cat4);
|
||||
EXPECT_EQ_STR(cat4, "/foo/bar");
|
||||
free(cat4);
|
||||
|
||||
char* cat_empty1 = path_cat("", "/bar");
|
||||
|
||||
char *cat_empty1 = path_cat("", "/bar");
|
||||
EXPECT_NOT_NULL(cat_empty1);
|
||||
EXPECT_EQ_STR(cat_empty1, "/bar");
|
||||
free(cat_empty1);
|
||||
|
||||
char* cat_empty2 = path_cat("/foo", "");
|
||||
|
||||
char *cat_empty2 = path_cat("/foo", "");
|
||||
EXPECT_NOT_NULL(cat_empty2);
|
||||
EXPECT_EQ_STR(cat_empty2, "/foo");
|
||||
free(cat_empty2);
|
||||
|
||||
char* cat_null1 = path_cat(NULL, "/bar");
|
||||
|
||||
char *cat_null1 = path_cat(NULL, "/bar");
|
||||
EXPECT_NOT_NULL(cat_null1);
|
||||
EXPECT_EQ_STR(cat_null1, "/bar");
|
||||
free(cat_null1);
|
||||
|
||||
char* cat_null2 = path_cat("/foo", NULL);
|
||||
|
||||
char *cat_null2 = path_cat("/foo", NULL);
|
||||
EXPECT_NOT_NULL(cat_null2);
|
||||
EXPECT_EQ_STR(cat_null2, "/foo");
|
||||
free(cat_null2);
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
#include "test_stress.h"
|
||||
#include "test_utils.h"
|
||||
#include "queue.h"
|
||||
#include <threads.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define ITEMS_PER_PRODUCER 2500
|
||||
#define NUM_PRODUCERS 4
|
||||
#define NUM_CONSUMERS 4
|
||||
#define TOTAL_ITEMS (ITEMS_PER_PRODUCER * NUM_PRODUCERS)
|
||||
|
||||
typedef struct {
|
||||
Queue* q;
|
||||
mtx_t* mutex;
|
||||
cnd_t* cnd_empty;
|
||||
cnd_t* cnd_full;
|
||||
int producer_id;
|
||||
} ProducerCtx;
|
||||
|
||||
typedef struct {
|
||||
Queue* q;
|
||||
mtx_t* mutex;
|
||||
cnd_t* cnd_empty;
|
||||
cnd_t* cnd_full;
|
||||
volatile int* producers_remaining;
|
||||
volatile bool* producers_done;
|
||||
} ConsumerMPMC;
|
||||
|
||||
static int mpmc_producer_func(void* arg) {
|
||||
ProducerCtx* ctx = (ProducerCtx*)arg;
|
||||
for (int i = 1; i <= ITEMS_PER_PRODUCER; i++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
*val = ctx->producer_id * ITEMS_PER_PRODUCER + i;
|
||||
queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int mpmc_consumer_func(void* arg) {
|
||||
ConsumerMPMC* ctx = (ConsumerMPMC*)arg;
|
||||
while (true) {
|
||||
int* val = (int*)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full,
|
||||
(const bool*)ctx->producers_done);
|
||||
if (val == NULL)
|
||||
break;
|
||||
free(val);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_queue_mpmc_stress() {
|
||||
Queue* q = queue_create(16, NULL);
|
||||
mtx_t mutex;
|
||||
cnd_t cnd_empty;
|
||||
cnd_t cnd_full;
|
||||
|
||||
mtx_init(&mutex, mtx_plain);
|
||||
cnd_init(&cnd_empty);
|
||||
cnd_init(&cnd_full);
|
||||
|
||||
volatile int producers_remaining = NUM_PRODUCERS;
|
||||
volatile bool producers_done = false;
|
||||
|
||||
ConsumerMPMC cctx = {.q = q,
|
||||
.mutex = &mutex,
|
||||
.cnd_empty = &cnd_empty,
|
||||
.cnd_full = &cnd_full,
|
||||
.producers_remaining = &producers_remaining,
|
||||
.producers_done = &producers_done};
|
||||
|
||||
thrd_t consumers[NUM_CONSUMERS];
|
||||
for (int i = 0; i < NUM_CONSUMERS; i++) {
|
||||
int res = thrd_create(&consumers[i], mpmc_consumer_func, &cctx);
|
||||
EXPECT_EQ_INT(res, thrd_success);
|
||||
}
|
||||
|
||||
ProducerCtx pctxs[NUM_PRODUCERS];
|
||||
thrd_t producers[NUM_PRODUCERS];
|
||||
for (int i = 0; i < NUM_PRODUCERS; i++) {
|
||||
pctxs[i] = (ProducerCtx){
|
||||
.q = q, .mutex = &mutex, .cnd_empty = &cnd_empty, .cnd_full = &cnd_full, .producer_id = i};
|
||||
int res = thrd_create(&producers[i], mpmc_producer_func, &pctxs[i]);
|
||||
EXPECT_EQ_INT(res, thrd_success);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_PRODUCERS; i++) {
|
||||
thrd_join(producers[i], NULL);
|
||||
mtx_lock(&mutex);
|
||||
producers_remaining--;
|
||||
if (producers_remaining == 0)
|
||||
producers_done = true;
|
||||
cnd_broadcast(&cnd_empty);
|
||||
mtx_unlock(&mutex);
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_CONSUMERS; i++) {
|
||||
thrd_join(consumers[i], NULL);
|
||||
}
|
||||
|
||||
EXPECT_TRUE(queue_is_empty(q));
|
||||
|
||||
queue_destroy(q);
|
||||
mtx_destroy(&mutex);
|
||||
cnd_destroy(&cnd_empty);
|
||||
cnd_destroy(&cnd_full);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
Queue* q;
|
||||
mtx_t* mutex;
|
||||
cnd_t* cnd_empty;
|
||||
cnd_t* cnd_full;
|
||||
bool done;
|
||||
int items_sent;
|
||||
int items_received;
|
||||
} BackpressureCtx;
|
||||
|
||||
static int bp_producer_func(void* arg) {
|
||||
BackpressureCtx* ctx = (BackpressureCtx*)arg;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
*val = i + 1;
|
||||
queue_enqueue_multithreaded(ctx->q, val, ctx->mutex, ctx->cnd_empty, ctx->cnd_full);
|
||||
ctx->items_sent++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bp_consumer_func(void* arg) {
|
||||
BackpressureCtx* ctx = (BackpressureCtx*)arg;
|
||||
while (ctx->items_received < 5) {
|
||||
int* val = (int*)queue_dequeue_multithreaded(ctx->q, ctx->mutex, ctx->cnd_empty, ctx->cnd_full,
|
||||
&ctx->done);
|
||||
if (val == NULL)
|
||||
break;
|
||||
ctx->items_received++;
|
||||
free(val);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void test_queue_backpressure() {
|
||||
Queue* q = queue_create(1, NULL);
|
||||
mtx_t mutex;
|
||||
cnd_t cnd_empty;
|
||||
cnd_t cnd_full;
|
||||
|
||||
mtx_init(&mutex, mtx_plain);
|
||||
cnd_init(&cnd_empty);
|
||||
cnd_init(&cnd_full);
|
||||
|
||||
BackpressureCtx ctx = {.q = q,
|
||||
.mutex = &mutex,
|
||||
.cnd_empty = &cnd_empty,
|
||||
.cnd_full = &cnd_full,
|
||||
.items_sent = 0,
|
||||
.items_received = 0,
|
||||
.done = false};
|
||||
|
||||
thrd_t producer, consumer;
|
||||
int res;
|
||||
|
||||
res = thrd_create(&consumer, bp_consumer_func, &ctx);
|
||||
EXPECT_EQ_INT(res, thrd_success);
|
||||
|
||||
res = thrd_create(&producer, bp_producer_func, &ctx);
|
||||
EXPECT_EQ_INT(res, thrd_success);
|
||||
|
||||
thrd_join(producer, NULL);
|
||||
|
||||
mtx_lock(&mutex);
|
||||
ctx.done = true;
|
||||
cnd_signal(&cnd_empty);
|
||||
mtx_unlock(&mutex);
|
||||
|
||||
thrd_join(consumer, NULL);
|
||||
|
||||
EXPECT_EQ_INT(ctx.items_sent, 5);
|
||||
EXPECT_EQ_INT(ctx.items_received, 5);
|
||||
EXPECT_TRUE(queue_is_empty(q));
|
||||
|
||||
queue_destroy(q);
|
||||
mtx_destroy(&mutex);
|
||||
cnd_destroy(&cnd_empty);
|
||||
cnd_destroy(&cnd_full);
|
||||
}
|
||||
|
||||
static void test_queue_rapid_create_destroy() {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
Queue* q = queue_create(4, free);
|
||||
EXPECT_NOT_NULL(q);
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
int* val = malloc(sizeof(int));
|
||||
*val = j;
|
||||
queue_enqueue(q, val);
|
||||
}
|
||||
|
||||
while (!queue_is_empty(q)) {
|
||||
void* v = queue_dequeue(q);
|
||||
free(v);
|
||||
}
|
||||
|
||||
queue_destroy(q);
|
||||
}
|
||||
}
|
||||
|
||||
void test_stress() {
|
||||
test_queue_mpmc_stress();
|
||||
test_queue_backpressure();
|
||||
test_queue_rapid_create_destroy();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#ifndef TEST_STRESS_H
|
||||
#define TEST_STRESS_H
|
||||
|
||||
void test_stress();
|
||||
|
||||
#endif
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "test_transport_ssh.h"
|
||||
#include "transport_ssh.h"
|
||||
#include "test_utils.h"
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* Test client_connect_ssh with invalid destination (missing colon) */
|
||||
static void test_ssh_connect_invalid_dest() {
|
||||
/* Missing colon — parse_remote_dest should fail and return NULL */
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Client* client = client_connect_ssh("invalid-destination-no-colon", 22, NULL);
|
||||
EXPECT_NULL(client);
|
||||
}
|
||||
|
||||
/* Test client_connect_ssh with empty destination */
|
||||
static void test_ssh_connect_empty_dest() {
|
||||
/* cppcheck-suppress constVariablePointer */
|
||||
Client* client = client_connect_ssh("", 22, NULL);
|
||||
EXPECT_NULL(client);
|
||||
}
|
||||
|
||||
/* Test client_connect_ssh with malformed destination (just a colon).
|
||||
* parse_remote_dest succeeds, ssh is exec'd and fails, but the function
|
||||
* creates a Client that must be cleaned up. */
|
||||
static void test_ssh_connect_malformed() {
|
||||
Client* client = client_connect_ssh(":", 22, NULL);
|
||||
/* ssh binary exists, so exec succeeds; the function returns a Client.
|
||||
* We just verify it doesn't crash and clean up properly. */
|
||||
if (client != NULL) {
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
}
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
/* Test client_connect_ssh with valid format but unreachable host.
|
||||
* The function launches ssh which will fail to connect, returns a Client. */
|
||||
static void test_ssh_connect_unreachable() {
|
||||
Client* client = client_connect_ssh("nonexistent.invalid:/remote/path", 22, NULL);
|
||||
if (client != NULL) {
|
||||
client_disconnect(client);
|
||||
client_delete(client);
|
||||
}
|
||||
EXPECT_TRUE(true);
|
||||
}
|
||||
|
||||
void test_transport_ssh() {
|
||||
test_ssh_connect_invalid_dest();
|
||||
test_ssh_connect_empty_dest();
|
||||
test_ssh_connect_malformed();
|
||||
test_ssh_connect_unreachable();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user