Skip to content

Commit 7a8c314

Browse files
authored
Merge pull request #6 from zombocoder/feature/support-symlink
Add comprehensive symlink support to BFC containers
2 parents 52fe28c + ae3105d commit 7a8c314

10 files changed

Lines changed: 816 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,75 @@ jobs:
101101
cd ..
102102
rm -rf extract_test
103103
104+
# Test symlink functionality
105+
echo "Testing symlink functionality..."
106+
107+
# Create test data with symlinks
108+
mkdir -p test_symlinks
109+
cd test_symlinks
110+
echo "Target file content" > target.txt
111+
mkdir subdir
112+
echo "Nested target" > subdir/nested.txt
113+
114+
# Create various types of symlinks
115+
ln -sf target.txt simple_link.txt
116+
ln -sf subdir/nested.txt relative_link.txt
117+
ln -sf /tmp/absolute_target absolute_link.txt
118+
ln -sf nonexistent_file broken_link.txt
119+
ln -sf subdir dir_link
120+
cd ..
121+
122+
# Test container creation with symlinks
123+
./build/bin/bfc create test_symlinks.bfc test_symlinks/
124+
echo "Container with symlinks created"
125+
126+
# Test symlink listing - verify 'l' prefix appears
127+
./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"
128+
./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"
129+
130+
# Test symlink counting in container info
131+
./build/bin/bfc info test_symlinks.bfc | grep "Symlinks:" && echo "Symlinks counted in container info" || echo "Symlink counting failed"
132+
133+
# Test symlink-specific info
134+
./build/bin/bfc info test_symlinks.bfc simple_link.txt | grep "Type: Symlink" && echo "Symlink type displayed correctly" || echo "Symlink type display failed"
135+
136+
# Test symlink extraction
137+
mkdir -p extract_symlinks
138+
cd extract_symlinks
139+
../build/bin/bfc extract ../test_symlinks.bfc
140+
141+
# Verify symlinks were extracted correctly
142+
[ -L simple_link.txt ] && echo "Simple symlink extracted as symlink" || echo "Simple symlink not extracted as symlink"
143+
[ -L broken_link.txt ] && echo "Broken symlink extracted as symlink" || echo "Broken symlink not extracted as symlink"
144+
145+
# Verify symlink targets
146+
if [ -L simple_link.txt ]; then
147+
TARGET=$(readlink simple_link.txt)
148+
[ "$TARGET" = "target.txt" ] && echo "Simple symlink target correct" || echo "Simple symlink target incorrect: $TARGET"
149+
fi
150+
151+
if [ -L absolute_link.txt ]; then
152+
TARGET=$(readlink absolute_link.txt)
153+
[ "$TARGET" = "/tmp/absolute_target" ] && echo "Absolute symlink target correct" || echo "Absolute symlink target incorrect: $TARGET"
154+
fi
155+
156+
cd ..
157+
rm -rf extract_symlinks
158+
159+
# Test symlinks with compression and encryption
160+
./build/bin/bfc create -c zstd -e testpass symlinks_comp_enc.bfc test_symlinks/simple_link.txt test_symlinks/absolute_link.txt
161+
echo "Symlinks work with compression and encryption"
162+
163+
mkdir -p extract_comp_enc
164+
cd extract_comp_enc
165+
../build/bin/bfc extract -p testpass ../symlinks_comp_enc.bfc
166+
[ -L simple_link.txt ] && echo "Compressed+encrypted symlink extracted correctly" || echo "Compressed+encrypted symlink extraction failed"
167+
cd ..
168+
rm -rf extract_comp_enc
169+
170+
# Clean up symlink test files
171+
rm -rf test_symlinks test_symlinks.bfc symlinks_comp_enc.bfc
172+
104173
# Test encryption functionality
105174
echo "Testing encryption features..."
106175
./build/bin/bfc create -e testpassword123 test_encrypted.bfc test_data/

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ configure:
4444
@echo "Configuring CMake build..."
4545
cmake -B $(BUILD_DIR) \
4646
-DCMAKE_BUILD_TYPE=$(CMAKE_BUILD_TYPE) \
47-
-DBFC_WITH_ZSTD=ON
47+
-DBFC_BUILD_TESTS=ON\
48+
-DBFC_WITH_ZSTD=ON \
4849
-DBFC_WITH_SODIUM=ON
4950

5051
# Build the project

include/bfc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ int bfc_create(const char* filename, uint32_t block_size, uint64_t features, bfc
6565
int bfc_add_file(bfc_t* w, const char* container_path, FILE* src, uint32_t mode, uint64_t mtime_ns,
6666
uint32_t* out_crc);
6767
int bfc_add_dir(bfc_t* w, const char* container_dir, uint32_t mode, uint64_t mtime_ns);
68+
int bfc_add_symlink(bfc_t* w, const char* container_path, const char* link_target, uint32_t mode,
69+
uint64_t mtime_ns);
6870

6971
/* --- Compression Configuration --- */
7072
int bfc_set_compression(bfc_t* w, uint8_t comp_type, int level);

src/cli/cmd_create.c

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
#define _GNU_SOURCE
1718
#include "cli.h"
1819
#include <dirent.h>
1920
#include <errno.h>
@@ -232,6 +233,42 @@ static int add_file_to_container(bfc_t* writer, const char* file_path, const cha
232233
return 0;
233234
}
234235

236+
static int add_symlink_to_container(bfc_t* writer, const char* link_path,
237+
const char* container_path) {
238+
print_verbose("Adding symlink: %s -> %s", link_path, container_path);
239+
240+
// Read the symlink target
241+
char target[1024];
242+
ssize_t target_len = readlink(link_path, target, sizeof(target) - 1);
243+
if (target_len == -1) {
244+
print_error("Cannot readlink '%s': %s", link_path, strerror(errno));
245+
return -1;
246+
}
247+
target[target_len] = '\0';
248+
249+
// Get symlink stats
250+
struct stat st;
251+
if (lstat(link_path, &st) != 0) {
252+
print_error("Cannot lstat symlink '%s': %s", link_path, strerror(errno));
253+
return -1;
254+
}
255+
256+
// Add symlink to container
257+
uint64_t mtime_ns = (uint64_t) st.st_mtime * 1000000000ULL;
258+
int result = bfc_add_symlink(writer, container_path, target, st.st_mode & 0777, mtime_ns);
259+
260+
if (result != BFC_OK) {
261+
print_error("Failed to add symlink '%s': %s", container_path, bfc_error_string(result));
262+
return -1;
263+
}
264+
265+
if (!g_options.quiet) {
266+
printf("Added: %s -> %s\n", container_path, target);
267+
}
268+
269+
return 0;
270+
}
271+
235272
static int add_directory_to_container(bfc_t* writer, const char* dir_path,
236273
const char* container_path);
237274

@@ -249,15 +286,17 @@ static int process_directory_entry(bfc_t* writer, const char* base_path, const c
249286
}
250287

251288
struct stat st;
252-
if (stat(full_path, &st) != 0) {
253-
print_error("Cannot stat '%s': %s", full_path, strerror(errno));
289+
if (lstat(full_path, &st) != 0) {
290+
print_error("Cannot lstat '%s': %s", full_path, strerror(errno));
254291
return -1;
255292
}
256293

257294
if (S_ISREG(st.st_mode)) {
258295
return add_file_to_container(writer, full_path, container_path);
259296
} else if (S_ISDIR(st.st_mode)) {
260297
return add_directory_to_container(writer, full_path, container_path);
298+
} else if (S_ISLNK(st.st_mode)) {
299+
return add_symlink_to_container(writer, full_path, container_path);
261300
} else {
262301
print_verbose("Skipping special file: %s", full_path);
263302
return 0;
@@ -417,7 +456,7 @@ int cmd_create(int argc, char* argv[]) {
417456
const char* input_path = opts.input_paths[i];
418457

419458
struct stat st;
420-
if (stat(input_path, &st) != 0) {
459+
if (lstat(input_path, &st) != 0) {
421460
print_error("Cannot access '%s': %s", input_path, strerror(errno));
422461
bfc_close(writer);
423462
return 1;
@@ -448,8 +487,13 @@ int cmd_create(int argc, char* argv[]) {
448487
bfc_close(writer);
449488
return 1;
450489
}
490+
} else if (S_ISLNK(st.st_mode)) {
491+
if (add_symlink_to_container(writer, input_path, basename) != 0) {
492+
bfc_close(writer);
493+
return 1;
494+
}
451495
} else {
452-
print_error("'%s' is not a regular file or directory", input_path);
496+
print_error("'%s' is not a regular file, directory, or symlink", input_path);
453497
bfc_close(writer);
454498
return 1;
455499
}

src/cli/cmd_extract.c

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <stdlib.h>
2424
#include <string.h>
2525
#include <sys/stat.h>
26+
#include <sys/time.h>
2627
#include <time.h>
2728
#include <unistd.h>
2829

@@ -319,6 +320,67 @@ static int extract_directory(const char* output_path, const bfc_entry_t* entry,
319320
return 0;
320321
}
321322

323+
static int extract_symlink(bfc_t* reader, const bfc_entry_t* entry, const char* output_path,
324+
int force) {
325+
// Check if file exists
326+
struct stat st;
327+
if (lstat(output_path, &st) == 0) {
328+
if (!S_ISLNK(st.st_mode)) {
329+
if (!force) {
330+
print_error("'%s' exists but is not a symlink. Use -f to overwrite.", output_path);
331+
return -1;
332+
}
333+
}
334+
// Remove existing file/symlink
335+
if (unlink(output_path) != 0) {
336+
print_error("Cannot remove '%s': %s", output_path, strerror(errno));
337+
return -1;
338+
}
339+
}
340+
341+
// Read symlink target from container
342+
char* target = malloc(entry->size + 1);
343+
if (!target) {
344+
print_error("Out of memory");
345+
return -1;
346+
}
347+
348+
size_t bytes_read = bfc_read(reader, entry->path, 0, target, entry->size);
349+
if (bytes_read != entry->size) {
350+
print_error("Failed to read symlink target for '%s'", entry->path);
351+
free(target);
352+
return -1;
353+
}
354+
target[entry->size] = '\0';
355+
356+
// Create symlink
357+
if (symlink(target, output_path) != 0) {
358+
print_error("Cannot create symlink '%s' -> '%s': %s", output_path, target, strerror(errno));
359+
free(target);
360+
return -1;
361+
}
362+
363+
// Set timestamps using lutimes (for symlinks)
364+
struct timeval times[2] = {
365+
{.tv_sec = entry->mtime_ns / 1000000000ULL,
366+
.tv_usec = (entry->mtime_ns % 1000000000ULL) / 1000}, // atime = mtime
367+
{.tv_sec = entry->mtime_ns / 1000000000ULL,
368+
.tv_usec = (entry->mtime_ns % 1000000000ULL) / 1000} // mtime
369+
};
370+
371+
if (lutimes(output_path, times) != 0) {
372+
print_verbose("Warning: cannot set timestamps on symlink '%s': %s", output_path,
373+
strerror(errno));
374+
}
375+
376+
if (!g_options.quiet) {
377+
printf("Extracted: %s -> %s\n", output_path, target);
378+
}
379+
380+
free(target);
381+
return 0;
382+
}
383+
322384
// Extract callback structure
323385
typedef struct {
324386
extract_options_t* opts;
@@ -363,6 +425,8 @@ static int extract_entry_callback(const bfc_entry_t* entry, void* user) {
363425
result = extract_file(ctx->reader, entry, output_path, opts->force);
364426
} else if (S_ISDIR(entry->mode)) {
365427
result = extract_directory(output_path, entry, opts->force);
428+
} else if (S_ISLNK(entry->mode)) {
429+
result = extract_symlink(ctx->reader, entry, output_path, opts->force);
366430
} else {
367431
print_verbose("Skipping special file: %s", entry->path);
368432
return 0;

src/cli/cmd_info.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ typedef struct {
157157
int total_entries;
158158
int total_files;
159159
int total_dirs;
160+
int total_symlinks;
160161
uint64_t total_size;
161162
uint64_t total_compressed_size;
162163
int show_detailed;
@@ -173,6 +174,8 @@ static int stats_callback(const bfc_entry_t* entry, void* user) {
173174
ctx->total_compressed_size += entry->obj_size;
174175
} else if (S_ISDIR(entry->mode)) {
175176
ctx->total_dirs++;
177+
} else if (S_ISLNK(entry->mode)) {
178+
ctx->total_symlinks++;
176179
}
177180

178181
if (ctx->show_detailed) {
@@ -213,7 +216,7 @@ static void show_container_info(bfc_t* reader, const char* container_file, int s
213216
}
214217

215218
// Gather statistics
216-
stats_context_t ctx = {0, 0, 0, 0, 0, show_detailed};
219+
stats_context_t ctx = {0, 0, 0, 0, 0, 0, show_detailed};
217220

218221
if (show_detailed) {
219222
printf("Entries:\n");
@@ -236,6 +239,9 @@ static void show_container_info(bfc_t* reader, const char* container_file, int s
236239
printf(" Total entries: %d\n", ctx.total_entries);
237240
printf(" Files: %d\n", ctx.total_files);
238241
printf(" Directories: %d\n", ctx.total_dirs);
242+
if (ctx.total_symlinks > 0) {
243+
printf(" Symlinks: %d\n", ctx.total_symlinks);
244+
}
239245

240246
if (has_encryption) {
241247
printf(" Encryption: ChaCha20-Poly1305\n");
@@ -276,6 +282,7 @@ static void show_entry_info(bfc_t* reader, const char* path) {
276282
printf("Entry: %s\n", entry.path);
277283
printf("Type: %s\n", S_ISDIR(entry.mode) ? "Directory"
278284
: S_ISREG(entry.mode) ? "Regular file"
285+
: S_ISLNK(entry.mode) ? "Symlink"
279286
: "Special file");
280287
printf("Mode: %s (0%04o)\n", mode_str, entry.mode & 0777);
281288
printf("Size: %s\n", size_str);

src/cli/cmd_list.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ static void format_file_mode(uint32_t mode, char* buffer) {
116116
buffer[0] = 'd';
117117
else if (S_ISREG(mode))
118118
buffer[0] = '-';
119+
else if (S_ISLNK(mode))
120+
buffer[0] = 'l';
121+
else if (S_ISBLK(mode))
122+
buffer[0] = 'b';
123+
else if (S_ISCHR(mode))
124+
buffer[0] = 'c';
125+
else if (S_ISFIFO(mode))
126+
buffer[0] = 'p';
127+
else if (S_ISSOCK(mode))
128+
buffer[0] = 's';
119129
else
120130
buffer[0] = '?';
121131

0 commit comments

Comments
 (0)