From 08670cee1b5f7055088ed2dce58aa3c89199ac45 Mon Sep 17 00:00:00 2001 From: zombocoder Date: Sat, 13 Sep 2025 17:34:38 +0300 Subject: [PATCH 1/3] Add support for symlink handling in container operations - Implemented symlink addition in the container with `bfc_add_symlink`. - Added symlink extraction functionality in `extract_symlink`. - Updated directory processing to handle symlinks during extraction. - Enhanced info display to include symlink statistics. - Modified file mode formatting to recognize symlinks. --- Makefile | 3 +- include/bfc.h | 1 + src/cli/cmd_create.c | 50 +++++++++++++++++++++++-- src/cli/cmd_extract.c | 62 +++++++++++++++++++++++++++++++ src/cli/cmd_info.c | 9 ++++- src/cli/cmd_list.c | 10 +++++ src/lib/bfc_writer.c | 85 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 214 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 839865b..d615539 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,8 @@ configure: @echo "Configuring CMake build..." cmake -B $(BUILD_DIR) \ -DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \ - -DBFC_WITH_ZSTD=ON + -DBFC_BUILD_TESTS=ON\ + -DBFC_WITH_ZSTD=ON \ -DBFC_WITH_SODIUM=ON # Build the project diff --git a/include/bfc.h b/include/bfc.h index 13f27ac..57e5ea9 100644 --- a/include/bfc.h +++ b/include/bfc.h @@ -65,6 +65,7 @@ int bfc_create(const char* filename, uint32_t block_size, uint64_t features, bfc int bfc_add_file(bfc_t* w, const char* container_path, FILE* src, uint32_t mode, uint64_t mtime_ns, uint32_t* out_crc); int bfc_add_dir(bfc_t* w, const char* container_dir, uint32_t mode, uint64_t mtime_ns); +int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, uint64_t mtime_ns); /* --- Compression Configuration --- */ int bfc_set_compression(bfc_t* w, uint8_t comp_type, int level); diff --git a/src/cli/cmd_create.c b/src/cli/cmd_create.c index cb740d6..067ada9 100644 --- a/src/cli/cmd_create.c +++ b/src/cli/cmd_create.c @@ -232,6 +232,41 @@ static int add_file_to_container(bfc_t* writer, const char* file_path, const cha return 0; } +static int add_symlink_to_container(bfc_t* writer, const char* link_path, const char* container_path) { + print_verbose("Adding symlink: %s -> %s", link_path, container_path); + + // Read the symlink target + char target[1024]; + ssize_t target_len = readlink(link_path, target, sizeof(target) - 1); + if (target_len == -1) { + print_error("Cannot readlink '%s': %s", link_path, strerror(errno)); + return -1; + } + target[target_len] = '\0'; + + // Get symlink stats + struct stat st; + if (lstat(link_path, &st) != 0) { + print_error("Cannot lstat symlink '%s': %s", link_path, strerror(errno)); + return -1; + } + + // Add symlink to container + uint64_t mtime_ns = (uint64_t) st.st_mtime * 1000000000ULL; + int result = bfc_add_symlink(writer, container_path, target, st.st_mode & 0777, mtime_ns); + + if (result != BFC_OK) { + print_error("Failed to add symlink '%s': %s", container_path, bfc_error_string(result)); + return -1; + } + + if (!g_options.quiet) { + printf("Added: %s -> %s\n", container_path, target); + } + + return 0; +} + static int add_directory_to_container(bfc_t* writer, const char* dir_path, const char* container_path); @@ -249,8 +284,8 @@ static int process_directory_entry(bfc_t* writer, const char* base_path, const c } struct stat st; - if (stat(full_path, &st) != 0) { - print_error("Cannot stat '%s': %s", full_path, strerror(errno)); + if (lstat(full_path, &st) != 0) { + print_error("Cannot lstat '%s': %s", full_path, strerror(errno)); return -1; } @@ -258,6 +293,8 @@ static int process_directory_entry(bfc_t* writer, const char* base_path, const c return add_file_to_container(writer, full_path, container_path); } else if (S_ISDIR(st.st_mode)) { return add_directory_to_container(writer, full_path, container_path); + } else if (S_ISLNK(st.st_mode)) { + return add_symlink_to_container(writer, full_path, container_path); } else { print_verbose("Skipping special file: %s", full_path); return 0; @@ -417,7 +454,7 @@ int cmd_create(int argc, char* argv[]) { const char* input_path = opts.input_paths[i]; struct stat st; - if (stat(input_path, &st) != 0) { + if (lstat(input_path, &st) != 0) { print_error("Cannot access '%s': %s", input_path, strerror(errno)); bfc_close(writer); return 1; @@ -448,8 +485,13 @@ int cmd_create(int argc, char* argv[]) { bfc_close(writer); return 1; } + } else if (S_ISLNK(st.st_mode)) { + if (add_symlink_to_container(writer, input_path, basename) != 0) { + bfc_close(writer); + return 1; + } } else { - print_error("'%s' is not a regular file or directory", input_path); + print_error("'%s' is not a regular file, directory, or symlink", input_path); bfc_close(writer); return 1; } diff --git a/src/cli/cmd_extract.c b/src/cli/cmd_extract.c index aa2eb89..ed1086d 100644 --- a/src/cli/cmd_extract.c +++ b/src/cli/cmd_extract.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -319,6 +320,65 @@ static int extract_directory(const char* output_path, const bfc_entry_t* entry, return 0; } +static int extract_symlink(bfc_t* reader, const bfc_entry_t* entry, const char* output_path, int force) { + // Check if file exists + struct stat st; + if (lstat(output_path, &st) == 0) { + if (!S_ISLNK(st.st_mode)) { + if (!force) { + print_error("'%s' exists but is not a symlink. Use -f to overwrite.", output_path); + return -1; + } + } + // Remove existing file/symlink + if (unlink(output_path) != 0) { + print_error("Cannot remove '%s': %s", output_path, strerror(errno)); + return -1; + } + } + + // Read symlink target from container + char* target = malloc(entry->size + 1); + if (!target) { + print_error("Out of memory"); + return -1; + } + + size_t bytes_read = bfc_read(reader, entry->path, 0, target, entry->size); + if (bytes_read != entry->size) { + print_error("Failed to read symlink target for '%s'", entry->path); + free(target); + return -1; + } + target[entry->size] = '\0'; + + // Create symlink + if (symlink(target, output_path) != 0) { + print_error("Cannot create symlink '%s' -> '%s': %s", output_path, target, strerror(errno)); + free(target); + return -1; + } + + // Set timestamps using lutimes (for symlinks) + struct timeval times[2] = { + {.tv_sec = entry->mtime_ns / 1000000000ULL, + .tv_usec = (entry->mtime_ns % 1000000000ULL) / 1000}, // atime = mtime + {.tv_sec = entry->mtime_ns / 1000000000ULL, + .tv_usec = (entry->mtime_ns % 1000000000ULL) / 1000} // mtime + }; + + if (lutimes(output_path, times) != 0) { + print_verbose("Warning: cannot set timestamps on symlink '%s': %s", output_path, strerror(errno)); + } + + if (!g_options.quiet) { + printf("Extracted: %s -> %s\n", output_path, target); + } + + free(target); + return 0; +} + // Extract callback structure typedef struct { extract_options_t* opts; @@ -363,6 +423,8 @@ static int extract_entry_callback(const bfc_entry_t* entry, void* user) { result = extract_file(ctx->reader, entry, output_path, opts->force); } else if (S_ISDIR(entry->mode)) { result = extract_directory(output_path, entry, opts->force); + } else if (S_ISLNK(entry->mode)) { + result = extract_symlink(ctx->reader, entry, output_path, opts->force); } else { print_verbose("Skipping special file: %s", entry->path); return 0; diff --git a/src/cli/cmd_info.c b/src/cli/cmd_info.c index 8b8d977..c782efc 100644 --- a/src/cli/cmd_info.c +++ b/src/cli/cmd_info.c @@ -157,6 +157,7 @@ typedef struct { int total_entries; int total_files; int total_dirs; + int total_symlinks; uint64_t total_size; uint64_t total_compressed_size; int show_detailed; @@ -173,6 +174,8 @@ static int stats_callback(const bfc_entry_t* entry, void* user) { ctx->total_compressed_size += entry->obj_size; } else if (S_ISDIR(entry->mode)) { ctx->total_dirs++; + } else if (S_ISLNK(entry->mode)) { + ctx->total_symlinks++; } if (ctx->show_detailed) { @@ -213,7 +216,7 @@ static void show_container_info(bfc_t* reader, const char* container_file, int s } // Gather statistics - stats_context_t ctx = {0, 0, 0, 0, 0, show_detailed}; + stats_context_t ctx = {0, 0, 0, 0, 0, 0, show_detailed}; if (show_detailed) { printf("Entries:\n"); @@ -236,6 +239,9 @@ static void show_container_info(bfc_t* reader, const char* container_file, int s printf(" Total entries: %d\n", ctx.total_entries); printf(" Files: %d\n", ctx.total_files); printf(" Directories: %d\n", ctx.total_dirs); + if (ctx.total_symlinks > 0) { + printf(" Symlinks: %d\n", ctx.total_symlinks); + } if (has_encryption) { printf(" Encryption: ChaCha20-Poly1305\n"); @@ -276,6 +282,7 @@ static void show_entry_info(bfc_t* reader, const char* path) { printf("Entry: %s\n", entry.path); printf("Type: %s\n", S_ISDIR(entry.mode) ? "Directory" : S_ISREG(entry.mode) ? "Regular file" + : S_ISLNK(entry.mode) ? "Symlink" : "Special file"); printf("Mode: %s (0%04o)\n", mode_str, entry.mode & 0777); printf("Size: %s\n", size_str); diff --git a/src/cli/cmd_list.c b/src/cli/cmd_list.c index e0f8275..b9a85e0 100644 --- a/src/cli/cmd_list.c +++ b/src/cli/cmd_list.c @@ -116,6 +116,16 @@ static void format_file_mode(uint32_t mode, char* buffer) { buffer[0] = 'd'; else if (S_ISREG(mode)) buffer[0] = '-'; + else if (S_ISLNK(mode)) + buffer[0] = 'l'; + else if (S_ISBLK(mode)) + buffer[0] = 'b'; + else if (S_ISCHR(mode)) + buffer[0] = 'c'; + else if (S_ISFIFO(mode)) + buffer[0] = 'p'; + else if (S_ISSOCK(mode)) + buffer[0] = 's'; else buffer[0] = '?'; diff --git a/src/lib/bfc_writer.c b/src/lib/bfc_writer.c index 06c3509..8f0d352 100644 --- a/src/lib/bfc_writer.c +++ b/src/lib/bfc_writer.c @@ -524,6 +524,91 @@ int bfc_add_dir(bfc_t* w, const char* container_dir, uint32_t mode, uint64_t mti return result; } +int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, uint64_t mtime_ns) { + if (!w || !container_path || !link_target || w->finished) { + return BFC_E_INVAL; + } + + // Normalize path + char* norm_path; + int result = bfc_path_normalize(container_path, &norm_path); + if (result != BFC_OK) { + return result; + } + + uint64_t obj_start = w->current_offset; + size_t target_len = strlen(link_target); + + // Create object header for symlink + struct bfc_obj_hdr obj_hdr = {.type = BFC_TYPE_SYMLINK, + .comp = BFC_COMP_NONE, + .enc = BFC_ENC_NONE, + .reserved = 0, + .name_len = (uint16_t) strlen(norm_path), + .padding = 0, + .mode = mode | S_IFLNK, // Add symlink type bits + .mtime_ns = mtime_ns, + .orig_size = target_len, + .enc_size = target_len, + .crc32c = 0}; // Will be calculated below + + // Calculate CRC32C of link target + uint32_t crc = bfc_crc32c_compute(link_target, target_len); + obj_hdr.crc32c = crc; + + // Write object header + if (fwrite(&obj_hdr, 1, sizeof(obj_hdr), w->file) != sizeof(obj_hdr)) { + bfc_path_free(norm_path); + return BFC_E_IO; + } + + // Write path + if (fwrite(norm_path, 1, obj_hdr.name_len, w->file) != obj_hdr.name_len) { + bfc_path_free(norm_path); + return BFC_E_IO; + } + + // Write padding after path to 16-byte boundary + size_t hdr_name_size = sizeof(obj_hdr) + obj_hdr.name_len; + size_t padding = bfc_padding_size(hdr_name_size, BFC_ALIGN); + if (padding > 0) { + uint8_t pad[BFC_ALIGN] = {0}; + if (fwrite(pad, 1, padding, w->file) != padding) { + bfc_path_free(norm_path); + return BFC_E_IO; + } + } + + // Write link target data + if (fwrite(link_target, 1, target_len, w->file) != target_len) { + bfc_path_free(norm_path); + return BFC_E_IO; + } + + // Write padding after target to 16-byte boundary + size_t target_padding = bfc_padding_size(target_len, BFC_ALIGN); + if (target_padding > 0) { + uint8_t pad[BFC_ALIGN] = {0}; + if (fwrite(pad, 1, target_padding, w->file) != target_padding) { + bfc_path_free(norm_path); + return BFC_E_IO; + } + } + + uint64_t obj_size = sizeof(obj_hdr) + obj_hdr.name_len + padding + target_len + target_padding; + + // Add to index + result = add_path_to_index(w, norm_path, obj_start, obj_size, mode | S_IFLNK, mtime_ns, + BFC_COMP_NONE, BFC_ENC_NONE, target_len, crc); + + if (result == BFC_OK) { + w->current_offset = obj_start + obj_size; + } + + bfc_path_free(norm_path); + return result; +} + static int index_entry_compare(const void* a, const void* b) { const bfc_index_entry_t* ea = (const bfc_index_entry_t*) a; const bfc_index_entry_t* eb = (const bfc_index_entry_t*) b; From 48241e3990fe51923423500ae5b97f694283140b Mon Sep 17 00:00:00 2001 From: zombocoder Date: Sat, 13 Sep 2025 17:53:45 +0300 Subject: [PATCH 2/3] Add symlink support with comprehensive tests and CI integration --- .github/workflows/ci.yml | 69 ++++++++++ include/bfc.h | 3 +- src/cli/cmd_create.c | 3 +- src/cli/cmd_extract.c | 6 +- src/lib/bfc_writer.c | 3 +- tests/unit/test_reader.c | 291 +++++++++++++++++++++++++++++++++++++++ tests/unit/test_writer.c | 236 +++++++++++++++++++++++++++++++ 7 files changed, 606 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efac6f3..5ac7f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -101,6 +101,75 @@ jobs: cd .. rm -rf extract_test + # Test symlink functionality + echo "Testing symlink functionality..." + + # Create test data with symlinks + mkdir -p test_symlinks + cd test_symlinks + echo "Target file content" > target.txt + mkdir subdir + echo "Nested target" > subdir/nested.txt + + # Create various types of symlinks + ln -sf target.txt simple_link.txt + ln -sf subdir/nested.txt relative_link.txt + ln -sf /tmp/absolute_target absolute_link.txt + ln -sf nonexistent_file broken_link.txt + ln -sf subdir dir_link + cd .. + + # Test container creation with symlinks + ./build/bin/bfc create test_symlinks.bfc test_symlinks/ + echo "Container with symlinks created" + + # Test symlink listing - verify 'l' prefix appears + ./build/bin/bfc list -l test_symlinks.bfc | grep "lrwxr-xr-x.*simple_link.txt" && echo "Simple symlink listed correctly" || echo "Simple symlink listing failed" + ./build/bin/bfc list -l test_symlinks.bfc | grep "lrwxr-xr-x.*absolute_link.txt" && echo "Absolute symlink listed correctly" || echo "Absolute symlink listing failed" + + # Test symlink counting in container info + ./build/bin/bfc info test_symlinks.bfc | grep "Symlinks:" && echo "Symlinks counted in container info" || echo "Symlink counting failed" + + # Test symlink-specific info + ./build/bin/bfc info test_symlinks.bfc simple_link.txt | grep "Type: Symlink" && echo "Symlink type displayed correctly" || echo "Symlink type display failed" + + # Test symlink extraction + mkdir -p extract_symlinks + cd extract_symlinks + ../build/bin/bfc extract ../test_symlinks.bfc + + # Verify symlinks were extracted correctly + [ -L simple_link.txt ] && echo "Simple symlink extracted as symlink" || echo "Simple symlink not extracted as symlink" + [ -L broken_link.txt ] && echo "Broken symlink extracted as symlink" || echo "Broken symlink not extracted as symlink" + + # Verify symlink targets + if [ -L simple_link.txt ]; then + TARGET=$(readlink simple_link.txt) + [ "$TARGET" = "target.txt" ] && echo "Simple symlink target correct" || echo "Simple symlink target incorrect: $TARGET" + fi + + if [ -L absolute_link.txt ]; then + TARGET=$(readlink absolute_link.txt) + [ "$TARGET" = "/tmp/absolute_target" ] && echo "Absolute symlink target correct" || echo "Absolute symlink target incorrect: $TARGET" + fi + + cd .. + rm -rf extract_symlinks + + # Test symlinks with compression and encryption + ./build/bin/bfc create -c zstd -e testpass symlinks_comp_enc.bfc test_symlinks/simple_link.txt test_symlinks/absolute_link.txt + echo "Symlinks work with compression and encryption" + + mkdir -p extract_comp_enc + cd extract_comp_enc + ../build/bin/bfc extract -p testpass ../symlinks_comp_enc.bfc + [ -L simple_link.txt ] && echo "Compressed+encrypted symlink extracted correctly" || echo "Compressed+encrypted symlink extraction failed" + cd .. + rm -rf extract_comp_enc + + # Clean up symlink test files + rm -rf test_symlinks test_symlinks.bfc symlinks_comp_enc.bfc + # Test encryption functionality echo "Testing encryption features..." ./build/bin/bfc create -e testpassword123 test_encrypted.bfc test_data/ diff --git a/include/bfc.h b/include/bfc.h index 57e5ea9..0edc6b2 100644 --- a/include/bfc.h +++ b/include/bfc.h @@ -65,7 +65,8 @@ int bfc_create(const char* filename, uint32_t block_size, uint64_t features, bfc int bfc_add_file(bfc_t* w, const char* container_path, FILE* src, uint32_t mode, uint64_t mtime_ns, uint32_t* out_crc); int bfc_add_dir(bfc_t* w, const char* container_dir, uint32_t mode, uint64_t mtime_ns); -int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, uint64_t mtime_ns); +int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, + uint64_t mtime_ns); /* --- Compression Configuration --- */ int bfc_set_compression(bfc_t* w, uint8_t comp_type, int level); diff --git a/src/cli/cmd_create.c b/src/cli/cmd_create.c index 067ada9..24f047f 100644 --- a/src/cli/cmd_create.c +++ b/src/cli/cmd_create.c @@ -232,7 +232,8 @@ static int add_file_to_container(bfc_t* writer, const char* file_path, const cha return 0; } -static int add_symlink_to_container(bfc_t* writer, const char* link_path, const char* container_path) { +static int add_symlink_to_container(bfc_t* writer, const char* link_path, + const char* container_path) { print_verbose("Adding symlink: %s -> %s", link_path, container_path); // Read the symlink target diff --git a/src/cli/cmd_extract.c b/src/cli/cmd_extract.c index ed1086d..117587f 100644 --- a/src/cli/cmd_extract.c +++ b/src/cli/cmd_extract.c @@ -320,7 +320,8 @@ static int extract_directory(const char* output_path, const bfc_entry_t* entry, return 0; } -static int extract_symlink(bfc_t* reader, const bfc_entry_t* entry, const char* output_path, int force) { +static int extract_symlink(bfc_t* reader, const bfc_entry_t* entry, const char* output_path, + int force) { // Check if file exists struct stat st; if (lstat(output_path, &st) == 0) { @@ -368,7 +369,8 @@ static int extract_symlink(bfc_t* reader, const bfc_entry_t* entry, const char* }; if (lutimes(output_path, times) != 0) { - print_verbose("Warning: cannot set timestamps on symlink '%s': %s", output_path, strerror(errno)); + print_verbose("Warning: cannot set timestamps on symlink '%s': %s", output_path, + strerror(errno)); } if (!g_options.quiet) { diff --git a/src/lib/bfc_writer.c b/src/lib/bfc_writer.c index 8f0d352..3bedfa2 100644 --- a/src/lib/bfc_writer.c +++ b/src/lib/bfc_writer.c @@ -524,7 +524,8 @@ int bfc_add_dir(bfc_t* w, const char* container_dir, uint32_t mode, uint64_t mti return result; } -int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, uint64_t mtime_ns) { +int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode, + uint64_t mtime_ns) { if (!w || !container_path || !link_target || w->finished) { return BFC_E_INVAL; } diff --git a/tests/unit/test_reader.c b/tests/unit/test_reader.c index b04459c..5964389 100644 --- a/tests/unit/test_reader.c +++ b/tests/unit/test_reader.c @@ -971,6 +971,289 @@ static int test_read_errors(void) { return 0; } +// Helper function to create a container with symlinks for testing +static int create_symlink_test_container(const char* filename) { + unlink(filename); + + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + if (result != BFC_OK) + return result; + + // Add a directory + result = bfc_add_dir(writer, "testdir", 0755, bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + // Add a regular file + const char* content = "Test file content for symlinks"; + const char* src_file = "/tmp/reader_test_src_symlink.txt"; + FILE* src = fopen(src_file, "w"); + if (!src) { + bfc_close(writer); + return BFC_E_IO; + } + fwrite(content, 1, strlen(content), src); + fclose(src); + + src = fopen(src_file, "rb"); + if (!src) { + bfc_close(writer); + return BFC_E_IO; + } + uint32_t crc; + result = bfc_add_file(writer, "testfile.txt", src, 0644, bfc_os_current_time_ns(), &crc); + fclose(src); + unlink(src_file); + + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + // Add various types of symlinks + result = bfc_add_symlink(writer, "link_to_file", "testfile.txt", 0755, bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + result = bfc_add_symlink(writer, "link_to_dir", "testdir", 0755, bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + result = bfc_add_symlink(writer, "absolute_link", "/tmp/absolute_target", 0755, + bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + result = + bfc_add_symlink(writer, "relative_link", "../parent/target", 0755, bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + result = + bfc_add_symlink(writer, "broken_link", "nonexistent_target", 0755, bfc_os_current_time_ns()); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + result = bfc_finish(writer); + if (result != BFC_OK) { + bfc_close(writer); + return result; + } + + bfc_close(writer); + return BFC_OK; +} + +// Helper types and callback for symlink listing test +typedef struct { + int total_count; + int symlink_count; + int file_count; + int dir_count; +} symlink_count_context_t; + +static int symlink_list_callback(const bfc_entry_t* entry, void* user) { + symlink_count_context_t* c = (symlink_count_context_t*) user; + c->total_count++; + + if (S_ISLNK(entry->mode)) { + c->symlink_count++; + } else if (S_ISREG(entry->mode)) { + c->file_count++; + } else if (S_ISDIR(entry->mode)) { + c->dir_count++; + } + + return 0; +} + +static int test_read_symlink_stat(void) { + const char* filename = "/tmp/reader_test_symlink_stat.bfc"; + + // Create test container + int result = create_symlink_test_container(filename); + assert(result == BFC_OK); + + // Open for reading + bfc_t* reader = NULL; + result = bfc_open(filename, &reader); + assert(result == BFC_OK); + assert(reader != NULL); + + // Test stat on symlinks + bfc_entry_t entry; + + // Test simple symlink + result = bfc_stat(reader, "link_to_file", &entry); + assert(result == BFC_OK); + assert(S_ISLNK(entry.mode)); + assert(entry.size == strlen("testfile.txt")); + + // Test directory symlink + result = bfc_stat(reader, "link_to_dir", &entry); + assert(result == BFC_OK); + assert(S_ISLNK(entry.mode)); + assert(entry.size == strlen("testdir")); + + // Test absolute symlink + result = bfc_stat(reader, "absolute_link", &entry); + assert(result == BFC_OK); + assert(S_ISLNK(entry.mode)); + assert(entry.size == strlen("/tmp/absolute_target")); + + // Test relative symlink + result = bfc_stat(reader, "relative_link", &entry); + assert(result == BFC_OK); + assert(S_ISLNK(entry.mode)); + assert(entry.size == strlen("../parent/target")); + + // Test broken symlink + result = bfc_stat(reader, "broken_link", &entry); + assert(result == BFC_OK); + assert(S_ISLNK(entry.mode)); + assert(entry.size == strlen("nonexistent_target")); + + bfc_close_read(reader); + unlink(filename); + + return 0; +} + +static int test_read_symlink_content(void) { + const char* filename = "/tmp/reader_test_symlink_content.bfc"; + + // Create test container + int result = create_symlink_test_container(filename); + assert(result == BFC_OK); + + // Open for reading + bfc_t* reader = NULL; + result = bfc_open(filename, &reader); + assert(result == BFC_OK); + + // Read symlink targets + char buffer[256]; + + // Test reading simple symlink target + size_t bytes_read = bfc_read(reader, "link_to_file", 0, buffer, sizeof(buffer)); + assert(bytes_read == strlen("testfile.txt")); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "testfile.txt") == 0); + + // Test reading directory symlink target + memset(buffer, 0, sizeof(buffer)); + bytes_read = bfc_read(reader, "link_to_dir", 0, buffer, sizeof(buffer)); + assert(bytes_read == strlen("testdir")); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "testdir") == 0); + + // Test reading absolute symlink target + memset(buffer, 0, sizeof(buffer)); + bytes_read = bfc_read(reader, "absolute_link", 0, buffer, sizeof(buffer)); + assert(bytes_read == strlen("/tmp/absolute_target")); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "/tmp/absolute_target") == 0); + + // Test reading relative symlink target + memset(buffer, 0, sizeof(buffer)); + bytes_read = bfc_read(reader, "relative_link", 0, buffer, sizeof(buffer)); + assert(bytes_read == strlen("../parent/target")); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "../parent/target") == 0); + + // Test reading broken symlink target + memset(buffer, 0, sizeof(buffer)); + bytes_read = bfc_read(reader, "broken_link", 0, buffer, sizeof(buffer)); + assert(bytes_read == strlen("nonexistent_target")); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "nonexistent_target") == 0); + + bfc_close_read(reader); + unlink(filename); + + return 0; +} + +static int test_symlink_listing(void) { + const char* filename = "/tmp/reader_test_symlink_list.bfc"; + + // Create test container + int result = create_symlink_test_container(filename); + assert(result == BFC_OK); + + // Open for reading + bfc_t* reader = NULL; + result = bfc_open(filename, &reader); + assert(result == BFC_OK); + + // Count entries and verify symlinks are listed + symlink_count_context_t ctx = {0, 0, 0, 0}; + + // List callback to count entries - defined as a static function above + result = bfc_list(reader, NULL, symlink_list_callback, &ctx); + assert(result == BFC_OK); + + // Verify counts: 1 dir + 1 file + 5 symlinks = 7 total + assert(ctx.total_count == 7); + assert(ctx.dir_count == 1); + assert(ctx.file_count == 1); + assert(ctx.symlink_count == 5); + + bfc_close_read(reader); + unlink(filename); + + return 0; +} + +static int test_symlink_partial_read(void) { + const char* filename = "/tmp/reader_test_symlink_partial.bfc"; + + // Create test container + int result = create_symlink_test_container(filename); + assert(result == BFC_OK); + + // Open for reading + bfc_t* reader = NULL; + result = bfc_open(filename, &reader); + assert(result == BFC_OK); + + // Test partial reads of symlink targets + char buffer[20]; + const char* target = "/tmp/absolute_target"; + size_t target_len = strlen(target); + + // Read first part of absolute symlink target + size_t bytes_read = bfc_read(reader, "absolute_link", 0, buffer, 5); + assert(bytes_read == 5); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "/tmp/") == 0); + + // Read second part - remaining bytes + size_t remaining = target_len - 5; + bytes_read = bfc_read(reader, "absolute_link", 5, buffer, remaining); + assert(bytes_read == remaining); + buffer[bytes_read] = '\0'; + assert(strcmp(buffer, "absolute_target") == 0); + + bfc_close_read(reader); + unlink(filename); + + return 0; +} + int test_reader(void) { if (test_open_container() != 0) return 1; @@ -1014,6 +1297,14 @@ int test_reader(void) { return 1; if (test_read_errors() != 0) return 1; + if (test_read_symlink_stat() != 0) + return 1; + if (test_read_symlink_content() != 0) + return 1; + if (test_symlink_listing() != 0) + return 1; + if (test_symlink_partial_read() != 0) + return 1; return 0; } \ No newline at end of file diff --git a/tests/unit/test_writer.c b/tests/unit/test_writer.c index 2503792..b524b29 100644 --- a/tests/unit/test_writer.c +++ b/tests/unit/test_writer.c @@ -544,6 +544,228 @@ static int test_finish_before_close(void) { return 0; } +static int test_add_simple_symlink(void) { + const char* filename = "/tmp/test_symlink.bfc"; + const char* target = "target.txt"; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add symlink + result = bfc_add_symlink(writer, "link.txt", target, 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_add_absolute_symlink(void) { + const char* filename = "/tmp/test_abs_symlink.bfc"; + const char* target = "/tmp/absolute_target.txt"; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add absolute symlink + result = bfc_add_symlink(writer, "abs_link.txt", target, 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_add_relative_symlink(void) { + const char* filename = "/tmp/test_rel_symlink.bfc"; + const char* target = "../parent/target.txt"; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add relative symlink + result = bfc_add_symlink(writer, "subdir/rel_link.txt", target, 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_symlink_parameter_validation(void) { + const char* filename = "/tmp/test_symlink_validation.bfc"; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Test invalid parameters + result = bfc_add_symlink(NULL, "link.txt", "target.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_E_INVAL); + + result = bfc_add_symlink(writer, NULL, "target.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_E_INVAL); + + result = bfc_add_symlink(writer, "link.txt", NULL, 0755, bfc_os_current_time_ns()); + assert(result == BFC_E_INVAL); + + // Valid symlink should work + result = + bfc_add_symlink(writer, "valid_link.txt", "valid_target.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_symlink_duplicate_paths(void) { + const char* filename = "/tmp/test_symlink_dup.bfc"; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add symlink + result = bfc_add_symlink(writer, "link.txt", "target1.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + // Try to add symlink with same path (should fail) + result = bfc_add_symlink(writer, "link.txt", "target2.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_E_EXISTS); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_symlink_long_target(void) { + const char* filename = "/tmp/test_symlink_long.bfc"; + + // Create a long target path + char long_target[1024]; + memset(long_target, 'a', sizeof(long_target) - 1); + long_target[sizeof(long_target) - 1] = '\0'; + + // Clean up + unlink(filename); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add symlink with long target + result = bfc_add_symlink(writer, "long_link.txt", long_target, 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + unlink(filename); + + return 0; +} + +static int test_mixed_content_with_symlinks(void) { + const char* filename = "/tmp/test_mixed_symlinks.bfc"; + const char* content = "Test file content"; + + // Clean up + unlink(filename); + + // Create temporary source file + const char* src_file = "/tmp/test_mixed_src.txt"; + FILE* src = fopen(src_file, "w"); + assert(src != NULL); + fwrite(content, 1, strlen(content), src); + fclose(src); + + // Create container + bfc_t* writer = NULL; + int result = bfc_create(filename, 4096, 0, &writer); + assert(result == BFC_OK); + + // Add directory + result = bfc_add_dir(writer, "testdir", 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + // Add file + src = fopen(src_file, "rb"); + assert(src != NULL); + uint32_t crc = 0; + result = bfc_add_file(writer, "testfile.txt", src, 0644, bfc_os_current_time_ns(), &crc); + assert(result == BFC_OK); + fclose(src); + + // Add symlinks + result = + bfc_add_symlink(writer, "link_to_file.txt", "testfile.txt", 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_add_symlink(writer, "link_to_dir", "testdir", 0755, bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_add_symlink(writer, "absolute_link.txt", "/tmp/absolute_target", 0755, + bfc_os_current_time_ns()); + assert(result == BFC_OK); + + result = bfc_finish(writer); + assert(result == BFC_OK); + + bfc_close(writer); + + // Clean up + unlink(src_file); + unlink(filename); + + return 0; +} + int test_writer(void) { if (test_create_empty_container() != 0) return 1; @@ -575,6 +797,20 @@ int test_writer(void) { return 1; if (test_finish_before_close() != 0) return 1; + if (test_add_simple_symlink() != 0) + return 1; + if (test_add_absolute_symlink() != 0) + return 1; + if (test_add_relative_symlink() != 0) + return 1; + if (test_symlink_parameter_validation() != 0) + return 1; + if (test_symlink_duplicate_paths() != 0) + return 1; + if (test_symlink_long_target() != 0) + return 1; + if (test_mixed_content_with_symlinks() != 0) + return 1; return 0; } \ No newline at end of file From ae3105d20191606cbcfa21ee5fdcb393a31636e6 Mon Sep 17 00:00:00 2001 From: zombocoder Date: Sat, 13 Sep 2025 17:57:53 +0300 Subject: [PATCH 3/3] Enable GNU extensions in cmd_create.c for enhanced compatibility --- src/cli/cmd_create.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/cmd_create.c b/src/cli/cmd_create.c index 24f047f..5b6d2fa 100644 --- a/src/cli/cmd_create.c +++ b/src/cli/cmd_create.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#define _GNU_SOURCE #include "cli.h" #include #include