Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ option(BFC_COVERAGE "Enable coverage reporting" OFF)
option(BFC_BUILD_BENCHMARKS "Build benchmarks" ON)
option(BFC_BUILD_EXAMPLES "Build examples" ON)

# Sanitizers, applied to Debug builds only.
#
# AddressSanitizer is off by default on macOS: as of macOS 26 its runtime
# deadlocks inside its own initialisation. It calls into dyld, dyld allocates,
# ASan intercepts that allocation and re-enters its initialiser, then spins
# forever on a lock it already holds. Every binary hangs before reaching
# main(), including `bfc --help`. This affects any program built with
# -fsanitize=address, not just this project, and a newer compiler-rt does not
# help. UndefinedBehaviorSanitizer is unaffected and stays on.
#
# Re-enable once the platform is fixed: cmake -DBFC_ASAN=ON
if(APPLE)
set(BFC_ASAN_DEFAULT OFF)
else()
set(BFC_ASAN_DEFAULT ON)
endif()
option(BFC_ASAN "Enable AddressSanitizer in Debug builds" ${BFC_ASAN_DEFAULT})
option(BFC_UBSAN "Enable UndefinedBehaviorSanitizer in Debug builds" ON)

# Automatically disable tests for Release builds to avoid unused variable warnings
if(CMAKE_BUILD_TYPE STREQUAL "Release")
option(BFC_BUILD_TESTS "Build tests" OFF)
Expand All @@ -37,7 +56,22 @@ endif()
# Compiler flags
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Werror")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0 -fsanitize=address,undefined")
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")

set(BFC_SANITIZERS "")
if(BFC_ASAN)
list(APPEND BFC_SANITIZERS address)
endif()
if(BFC_UBSAN)
list(APPEND BFC_SANITIZERS undefined)
endif()
if(BFC_SANITIZERS)
list(JOIN BFC_SANITIZERS "," BFC_SANITIZER_ARG)
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fsanitize=${BFC_SANITIZER_ARG}")
message(STATUS "Debug sanitizers: ${BFC_SANITIZER_ARG}")
else()
message(STATUS "Debug sanitizers: none")
endif()
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3 -DNDEBUG")

if(BFC_COVERAGE)
Expand Down
2 changes: 0 additions & 2 deletions benchmarks/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ endif()
# Link ZSTD if enabled (avoid duplicates by handling all targets together)
if(BFC_WITH_ZSTD)
foreach(target ${all_benchmark_targets})
target_link_libraries(${target} ${ZSTD_LIBRARIES})
target_link_directories(${target} PRIVATE ${ZSTD_LIBRARY_DIRS})
target_compile_definitions(${target} PRIVATE BFC_WITH_ZSTD)
endforeach()
Expand All @@ -76,7 +75,6 @@ endif()
# Link libsodium if enabled (avoid duplicates by handling all targets together)
if(BFC_WITH_SODIUM)
foreach(target ${all_benchmark_targets})
target_link_libraries(${target} ${SODIUM_LIBRARIES})
target_link_directories(${target} PRIVATE ${SODIUM_LIBRARY_DIRS})
target_compile_definitions(${target} PRIVATE BFC_WITH_SODIUM)
endforeach()
Expand Down
2 changes: 0 additions & 2 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ endif()
# Link ZSTD if enabled (avoid duplicates by handling all targets together)
if(BFC_WITH_ZSTD)
foreach(target ${all_example_targets})
target_link_libraries(${target} ${ZSTD_LIBRARIES})
target_link_directories(${target} PRIVATE ${ZSTD_LIBRARY_DIRS})
target_compile_definitions(${target} PRIVATE BFC_WITH_ZSTD)
endforeach()
Expand All @@ -63,7 +62,6 @@ endif()
# Link libsodium if enabled (avoid duplicates by handling all targets together)
if(BFC_WITH_SODIUM)
foreach(target ${all_example_targets})
target_link_libraries(${target} ${SODIUM_LIBRARIES})
target_link_directories(${target} PRIVATE ${SODIUM_LIBRARY_DIRS})
target_compile_definitions(${target} PRIVATE BFC_WITH_SODIUM)
endforeach()
Expand Down
2 changes: 0 additions & 2 deletions src/cli/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,11 @@ target_link_libraries(bfc_cli bfc)

# Link ZSTD if enabled (needed for static library dependencies)
if(BFC_WITH_ZSTD)
target_link_libraries(bfc_cli ${ZSTD_LIBRARIES})
target_link_directories(bfc_cli PRIVATE ${ZSTD_LIBRARY_DIRS})
endif()

# Link libsodium if enabled (needed for encryption support)
if(BFC_WITH_SODIUM)
target_link_libraries(bfc_cli ${SODIUM_LIBRARIES})
target_link_directories(bfc_cli PRIVATE ${SODIUM_LIBRARY_DIRS})
target_compile_definitions(bfc_cli PRIVATE BFC_WITH_SODIUM)
endif()
Expand Down
37 changes: 36 additions & 1 deletion src/lib/bfc_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ int bfc_list(bfc_t* r, const char* prefix_dir, bfc_list_cb cb, void* user) {
.mode = r->entries[i].mode,
.mtime_ns = r->entries[i].mtime_ns,
.comp = r->entries[i].comp,
.enc = r->entries[i].enc,
.size = r->entries[i].orig_size,
.crc32c = r->entries[i].crc32c,
.obj_offset = r->entries[i].obj_offset,
Expand Down Expand Up @@ -939,7 +940,41 @@ int bfc_verify(bfc_t* r, int deep) {
if (deep) {
// Deep verification - read and verify CRC of each file
if ((entry->mode & S_IFMT) == S_IFREG) {
// Verify file content CRC
// entry->crc32c covers the *original* content. When the object was
// compressed or encrypted the stored bytes are something else, so the
// checksum can only be recomputed after undoing those transforms.
// bfc_read already performs exactly that pipeline.
if (entry->comp != BFC_COMP_NONE || entry->enc != BFC_ENC_NONE) {
if (entry->enc != BFC_ENC_NONE && !r->has_encryption_key) {
// Encrypted content cannot be checked without a key. Saying so is
// honest; reporting corruption would not be.
return BFC_E_PERM;
}

if (entry->orig_size > 0) {
uint8_t* plaintext = bfc_malloc((size_t) entry->orig_size);
if (!plaintext) {
return BFC_E_IO;
}

size_t got = bfc_read(r, entry->path, 0, plaintext, (size_t) entry->orig_size);
if (got != entry->orig_size) {
bfc_free(plaintext);
return BFC_E_CRC;
}

uint32_t plain_crc = bfc_crc32c_compute(plaintext, got);
bfc_free(plaintext);

if (plain_crc != entry->crc32c) {
return BFC_E_CRC;
}
}
continue;
}

// Stored as-is: stream it, so a large file is not materialised in
// memory just to be checksummed.
if (bfc_os_seek(r->file, (int64_t) entry->obj_offset, SEEK_SET) != BFC_OK) {
return BFC_E_IO;
}
Expand Down
2 changes: 0 additions & 2 deletions tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,12 @@ target_link_libraries(unit_tests bfc)

# Link ZSTD if enabled (needed for static library dependencies)
if(BFC_WITH_ZSTD)
target_link_libraries(unit_tests ${ZSTD_LIBRARIES})
target_link_directories(unit_tests PRIVATE ${ZSTD_LIBRARY_DIRS})
target_compile_definitions(unit_tests PRIVATE BFC_WITH_ZSTD)
endif()

# Link libsodium if enabled (needed for encryption tests)
if(BFC_WITH_SODIUM)
target_link_libraries(unit_tests ${SODIUM_LIBRARIES})
target_link_directories(unit_tests PRIVATE ${SODIUM_LIBRARY_DIRS})
target_compile_definitions(unit_tests PRIVATE BFC_WITH_SODIUM)
endif()
Expand Down
183 changes: 183 additions & 0 deletions tests/unit/test_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -1363,8 +1363,187 @@ static int test_read_encrypted_uncompressed(void) {

return 0;
}

// Collect the compression and encryption reported for one path by bfc_list.
struct enc_probe {
const char* wanted;
int seen;
uint32_t comp;
uint32_t enc;
};

static int enc_probe_cb(const bfc_entry_t* entry, void* user) {
struct enc_probe* probe = (struct enc_probe*) user;
if (strcmp(entry->path, probe->wanted) == 0) {
probe->seen = 1;
probe->comp = entry->comp;
probe->enc = entry->enc;
}
return 0;
}

// Regression: bfc_list built its bfc_entry_t with a designated initializer that
// omitted `enc`, so every listed entry silently reported BFC_ENC_NONE even in an
// encrypted container. bfc_stat has always reported it, so the two disagreed.
static int test_list_reports_encryption(void) {
const char* filename = "reader_test_list_enc.bfc";
const char* content = "listed and encrypted";

uint8_t key[32];
for (size_t i = 0; i < sizeof(key); i++) {
key[i] = (uint8_t) (i * 3 + 1);
}

unlink(filename);

bfc_t* writer = NULL;
int result = bfc_create(filename, 4096, 0, &writer);
assert(result == BFC_OK);
assert(bfc_set_encryption_key(writer, key) == BFC_OK);

FILE* temp = tmpfile();
assert(temp != NULL);
fwrite(content, 1, strlen(content), temp);
rewind(temp);
assert(bfc_add_file(writer, "secret.txt", temp, 0644, bfc_os_current_time_ns(), NULL) == BFC_OK);
fclose(temp);

assert(bfc_finish(writer) == BFC_OK);
bfc_close(writer);

bfc_t* reader = NULL;
assert(bfc_open(filename, &reader) == BFC_OK);

bfc_entry_t stated;
assert(bfc_stat(reader, "secret.txt", &stated) == BFC_OK);
assert(stated.enc != BFC_ENC_NONE);

struct enc_probe probe = {.wanted = "secret.txt", .seen = 0, .comp = 0, .enc = 0};
assert(bfc_list(reader, NULL, enc_probe_cb, &probe) == BFC_OK);
assert(probe.seen == 1);

// What bfc_list reports must agree with bfc_stat.
assert(probe.enc == stated.enc);
assert(probe.comp == stated.comp);
assert(probe.enc != BFC_ENC_NONE);

bfc_close_read(reader);
unlink(filename);

return 0;
}
#endif

// Write a container holding one compressible file, optionally compressed and
// optionally encrypted with `key`. Returns BFC_OK on success.
static int make_verify_container(const char* filename, int compress, const uint8_t* key) {
unlink(filename);

bfc_t* writer = NULL;
int result = bfc_create(filename, 4096, 0, &writer);
if (result != BFC_OK) {
return result;
}

if (compress) {
result = bfc_set_compression(writer, BFC_COMP_ZSTD, 3);
if (result != BFC_OK) {
bfc_close(writer);
return result;
}
bfc_set_compression_threshold(writer, 16);
}
if (key) {
result = bfc_set_encryption_key(writer, key);
if (result != BFC_OK) {
bfc_close(writer);
return result;
}
}

FILE* temp = tmpfile();
if (!temp) {
bfc_close(writer);
return BFC_E_IO;
}
// Repetitive, so zstd actually shrinks it and the stored bytes differ from
// the original — which is the whole point of the test.
for (int i = 0; i < 400; i++) {
fputs("verify me verify me verify me\n", temp);
}
rewind(temp);

result = bfc_add_file(writer, "payload.txt", temp, 0644, bfc_os_current_time_ns(), NULL);
fclose(temp);
if (result != BFC_OK) {
bfc_close(writer);
return result;
}

result = bfc_finish(writer);
bfc_close(writer);
return result;
}

// Regression: bfc_verify(deep) checksummed the raw stored bytes against the
// checksum of the original content. For anything compressed or encrypted the
// two differ by definition, so it reported corruption on perfectly good
// archives — including ones it had just written itself.
static int test_verify_deep_transformed_content(void) {
const char* filename = "reader_test_verify_deep.bfc";

// Stored as-is: this path always worked, and must keep working.
assert(make_verify_container(filename, 0, NULL) == BFC_OK);
bfc_t* reader = NULL;
assert(bfc_open(filename, &reader) == BFC_OK);
assert(bfc_verify(reader, 0) == BFC_OK);
assert(bfc_verify(reader, 1) == BFC_OK);
bfc_close_read(reader);
unlink(filename);

#ifdef BFC_WITH_ZSTD
// Compressed: the stored bytes are a zstd frame, so the checksum can only be
// recomputed after decompressing.
assert(make_verify_container(filename, 1, NULL) == BFC_OK);
reader = NULL;
assert(bfc_open(filename, &reader) == BFC_OK);

bfc_entry_t entry;
assert(bfc_stat(reader, "payload.txt", &entry) == BFC_OK);
assert(entry.comp == BFC_COMP_ZSTD);
assert(entry.obj_size < entry.size); // it really did shrink

assert(bfc_verify(reader, 1) == BFC_OK);
bfc_close_read(reader);
unlink(filename);
#endif

#ifdef BFC_WITH_SODIUM
// Encrypted, with the key supplied: same story, plus decryption.
uint8_t key[32];
for (size_t i = 0; i < sizeof(key); i++) {
key[i] = (uint8_t) (i + 7);
}

assert(make_verify_container(filename, 1, key) == BFC_OK);
reader = NULL;
assert(bfc_open(filename, &reader) == BFC_OK);
assert(bfc_reader_set_encryption_key(reader, key) == BFC_OK);
assert(bfc_verify(reader, 1) == BFC_OK);
bfc_close_read(reader);

// Without the key the content cannot be checked at all. Saying so is honest;
// claiming corruption is not.
reader = NULL;
assert(bfc_open(filename, &reader) == BFC_OK);
assert(bfc_verify(reader, 1) == BFC_E_PERM);
bfc_close_read(reader);
unlink(filename);
#endif

return 0;
}

int test_reader(void) {
if (test_open_container() != 0)
return 1;
Expand Down Expand Up @@ -1419,7 +1598,11 @@ int test_reader(void) {
#ifdef BFC_WITH_SODIUM
if (test_read_encrypted_uncompressed() != 0)
return 1;
if (test_list_reports_encryption() != 0)
return 1;
#endif
if (test_verify_deep_transformed_content() != 0)
return 1;

return 0;
}
Loading