diff --git a/.github/workflows/freebsd-build.yml b/.github/workflows/freebsd-build.yml index 3b0d803..e5edf13 100644 --- a/.github/workflows/freebsd-build.yml +++ b/.github/workflows/freebsd-build.yml @@ -19,7 +19,7 @@ jobs: release: '14.3' usesh: true prepare: | - pkg install -y llvm + pkg install -y llvm kyua run: | # Print FreeBSD version @@ -56,4 +56,49 @@ jobs: # Check module information file bfcfs.ko - echo "All checks passed!" + echo "Build successful! Now running tests..." + + # Verify test fixture exists + if [ ! -f tests/fixtures/test.bfc ]; then + echo "Warning: Test fixture tests/fixtures/test.bfc not found, skipping tests" + echo "All checks passed (build only)!" + exit 0 + fi + + echo "Test container found, proceeding with tests..." + + # Load the kernel module + echo "Loading BFCFS kernel module..." + kldload ./bfcfs.ko || { + echo "ERROR: Failed to load kernel module" + kldstat + dmesg | tail -20 + exit 1 + } + + # Verify module is loaded + kldstat | grep bfcfs || { + echo "ERROR: Module not loaded" + exit 1 + } + + # Run the tests + echo "Running BFCFS tests..." + cd tests + kyua test || { + echo "Tests failed, showing report:" + kyua report --verbose + cd .. + kldunload bfcfs || true + exit 1 + } + + # Show test report + echo "Test results:" + kyua report + + # Unload module + cd .. + kldunload bfcfs + + echo "All checks and tests passed!" diff --git a/README.md b/README.md index 9da7388..61c65fa 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ Low-level I/O routines (pread, object reading, CRC verification) bfcfs-freebsd/ ├── .github/ │ └── workflows/ -│ └── freebsd-build.yml # GitHub Actions CI +│ └── freebsd-build.yml # GitHub Actions CI with tests ├── .gitignore # Git ignore rules ├── LICENSE # BSD 3-Clause License ├── Makefile # Build configuration @@ -139,7 +139,16 @@ bfcfs-freebsd/ ├── bfcfs_vfs.c # VFS operations ├── bfcfs_vnops.c # Vnode operations ├── bfc_format.c # BFC format parsing -└── bfc_io.c # I/O routines +├── bfc_io.c # I/O routines +└── tests/ # ATF test suite + ├── Kyuafile # Test configuration + ├── h_funcs.subr # Test helper functions + ├── mount_test # Mount/unmount tests + ├── read_test # Read operation tests + ├── readdir_test # Directory listing tests + ├── symlink_test # Symlink tests + └── fixtures/ # Test BFC containers + └── README.md # Fixture documentation ``` ## Debugging @@ -218,6 +227,28 @@ doas kldload ./bfcfs.ko ### Testing: +The module includes an ATF-based test suite using Kyua: + +```sh +# Install test dependencies +doas pkg install kyua + +# Load the module +doas kldload ./bfcfs.ko + +# Run the test suite +cd tests +doas kyua test + +# View test results +kyua report + +# Detailed report +kyua report --verbose +``` + +#### Manual Testing: + ```sh # Create a test mount point doas mkdir -p /mnt/bfc @@ -225,7 +256,7 @@ doas mkdir -p /mnt/bfc # Mount test container doas mount -t bfcfs -o ro /path/to/test.bfc /mnt/bfc -# Run tests +# Run manual tests ls -la /mnt/bfc cat /mnt/bfc/somefile.txt find /mnt/bfc -type f @@ -234,6 +265,21 @@ find /mnt/bfc -type f doas umount /mnt/bfc ``` +### Test Suite Structure: + +``` +tests/ +├── Kyuafile # Test configuration +├── h_funcs.subr # Helper functions +├── mount_test # Mount/unmount tests +├── read_test # Read operations tests +├── readdir_test # Directory listing tests +├── symlink_test # Symlink support tests +└── fixtures/ + ├── README.md # Fixture documentation + └── test.bfc # Test BFC container +``` + ## License BSD 3-Clause License - See [LICENSE](LICENSE) file for details @@ -315,12 +361,12 @@ doas mount -t bfcfs -o ro /var/containers/website.bfc /var/www/html | Feature | BFCFS | SquashFS | ISO9660 | FUSE | | -------------- | ----- | -------- | ------- | ------ | -| Read-only | ✅ | ✅ | ✅ | ❌ | -| Kernel module | ✅ | ✅ | ✅ | ❌ | -| Compression | 🚧 | ✅ | ❌ | Varies | -| Encryption | 🚧 | ❌ | ❌ | Varies | -| CRC integrity | ✅ | ✅ | ❌ | Varies | -| Cross-platform | ✅ | ✅ | ✅ | ✅ | +| Read-only | [x] | [x] | [x] | [-] | +| Kernel module | [x] | [x] | [x] | [-] | +| Compression | [ ] | [x] | [-] | Varies | +| Encryption | [ ] | [-] | [-] | Varies | +| CRC integrity | [x] | [x] | [-] | Varies | +| Cross-platform | [x] | [x] | [x] | [x] | | Performance | High | High | High | Medium | ## Project Status @@ -329,20 +375,20 @@ doas mount -t bfcfs -o ro /var/containers/website.bfc /var/www/html ### Implemented Features -- ✅ Mount/unmount operations -- ✅ Directory listing and navigation -- ✅ File reading (uncompressed) -- ✅ Symlink support -- ✅ CRC32C verification -- ✅ Hardware-accelerated checksums +- [x] Mount/unmount operations +- [x] Directory listing and navigation +- [x] File reading (uncompressed) +- [x] Symlink support +- [x] CRC32C verification +- [x] Hardware-accelerated checksums ### Planned Features (Roadmap) -- 🚧 ZSTD decompression support -- 🚧 ChaCha20-Poly1305 decryption support -- 🚧 Extended attributes -- 🚧 Advanced mount options -- 🚧 Performance optimizations +- [ ] ZSTD decompression support +- [ ] ChaCha20-Poly1305 decryption support +- [ ] Extended attributes +- [ ] Advanced mount options +- [ ] Performance optimizations ## Contributing diff --git a/bfc_format.c b/bfc_format.c index 4d9ff4e..9a0a120 100644 --- a/bfc_format.c +++ b/bfc_format.c @@ -33,14 +33,14 @@ bfc_read_header(struct bfcfs_mount *bmp, struct thread *td) /* Read header from offset 0 */ error = bfc_pread(bmp, hdr, sizeof(*hdr), 0, td); if (error) { - printf("bfcfs: failed to read header: %d\n", error); + BFCFS_ERR("failed to read header: %d", error); return (error); } /* Validate magic string */ - if (strncmp(hdr->magic, "BFCFv1", 6) != 0) { - printf("bfcfs: invalid magic: expected 'BFCFv1', got '%.8s'\n", - hdr->magic); + if (strncmp(hdr->magic, BFC_MAGIC_STR, strlen(BFC_MAGIC_STR)) != 0) { + BFCFS_ERR("invalid magic: expected '%s', got '%.8s'", + BFC_MAGIC_STR, hdr->magic); return (EINVAL); } @@ -52,9 +52,10 @@ bfc_read_header(struct bfcfs_mount *bmp, struct thread *td) BFCFS_DEBUG("format: header valid, block_size=%u, features=0x%lx", hdr->block_size, hdr->features); - /* Sanity check block size */ - if (hdr->block_size == 0 || hdr->block_size > 1048576) { - printf("bfcfs: invalid block size: %u\n", hdr->block_size); + /* Sanity check block size (must be non-zero and reasonable) */ + if (hdr->block_size == 0 || hdr->block_size > BFC_MAX_BLOCK_SIZE) { + BFCFS_ERR("invalid block size: %u (max %u)", + hdr->block_size, BFC_MAX_BLOCK_SIZE); return (EINVAL); } @@ -77,7 +78,7 @@ bfc_read_footer(struct bfcfs_mount *bmp, struct thread *td) return (error); if (file_size < (off_t)(BFC_HEADER_SIZE + BFC_FOOTER_SIZE)) { - printf("bfcfs: file too small: %ld bytes\n", file_size); + BFCFS_ERR("file too small: %ld bytes", file_size); return (EINVAL); } @@ -85,13 +86,13 @@ bfc_read_footer(struct bfcfs_mount *bmp, struct thread *td) footer_offset = file_size - BFC_FOOTER_SIZE; error = bfc_pread(bmp, footer, sizeof(*footer), footer_offset, td); if (error) { - printf("bfcfs: failed to read footer: %d\n", error); + BFCFS_ERR("failed to read footer: %d", error); return (error); } /* Validate footer magic */ - if (strncmp(footer->magic_start, "BFCFIDX", 7) != 0) { - printf("bfcfs: invalid footer magic\n"); + if (strncmp(footer->magic_start, BFC_INDEX_MAGIC, strlen(BFC_INDEX_MAGIC)) != 0) { + BFCFS_ERR("invalid footer magic"); return (EINVAL); } @@ -107,12 +108,13 @@ bfc_read_footer(struct bfcfs_mount *bmp, struct thread *td) /* Sanity checks */ if (footer->index_offset < BFC_HEADER_SIZE || footer->index_offset >= (uint64_t)file_size) { - printf("bfcfs: invalid index offset: %lu\n", footer->index_offset); + BFCFS_ERR("invalid index offset: %lu", footer->index_offset); return (EINVAL); } - if (footer->index_size == 0 || footer->index_size > 100*1024*1024) { - printf("bfcfs: invalid index size: %lu\n", footer->index_size); + if (footer->index_size == 0 || footer->index_size > BFC_MAX_INDEX_SIZE) { + BFCFS_ERR("invalid index size: %lu (max %u)", + footer->index_size, BFC_MAX_INDEX_SIZE); return (EINVAL); } @@ -142,18 +144,16 @@ bfc_parse_index_entry(const uint8_t *buf, size_t buflen, size_t *offset, size_t remaining = buflen - *offset; uint32_t path_len; - /* Need at least 4 bytes for path_len */ - if (remaining < 4) + /* Need at least BFC_PATH_LEN_SIZE bytes for path_len */ + if (remaining < BFC_PATH_LEN_SIZE) return (EINVAL); /* Read path length (32-bit) */ - bcopy(p, &path_len, 4); - path_len = LE32(path_len); - p += 4; - remaining -= 4; + READ_LE32(p, path_len); + remaining -= BFC_PATH_LEN_SIZE; /* Sanity check path length */ - if (path_len == 0 || path_len > 4096 || path_len > remaining) + if (path_len == 0 || path_len > BFC_MAX_PATH_LEN || path_len > remaining) return (EINVAL); /* Allocate and copy path */ @@ -163,42 +163,19 @@ bfc_parse_index_entry(const uint8_t *buf, size_t buflen, size_t *offset, p += path_len; remaining -= path_len; - /* Need at least 48 more bytes for metadata */ - if (remaining < 48) + /* Need at least metadata bytes for entry fields */ + if (remaining < BFC_INDEX_ENTRY_METADATA_SIZE) return (EINVAL); /* Parse metadata fields (all little-endian) */ - bcopy(p, &entry->obj_offset, 8); - entry->obj_offset = LE64(entry->obj_offset); - p += 8; - - bcopy(p, &entry->obj_size, 8); - entry->obj_size = LE64(entry->obj_size); - p += 8; - - bcopy(p, &entry->mode, 4); - entry->mode = LE32(entry->mode); - p += 4; - - bcopy(p, &entry->mtime_ns, 8); - entry->mtime_ns = LE64(entry->mtime_ns); - p += 8; - - bcopy(p, &entry->comp, 4); - entry->comp = LE32(entry->comp); - p += 4; - - bcopy(p, &entry->enc, 4); - entry->enc = LE32(entry->enc); - p += 4; - - bcopy(p, &entry->size, 8); /* orig_size */ - entry->size = LE64(entry->size); - p += 8; - - bcopy(p, &entry->crc32c, 4); - entry->crc32c = LE32(entry->crc32c); - p += 4; + READ_LE64(p, entry->obj_offset); + READ_LE64(p, entry->obj_size); + READ_LE32(p, entry->mode); + READ_LE64(p, entry->mtime_ns); + READ_LE32(p, entry->comp); + READ_LE32(p, entry->enc); + READ_LE64(p, entry->size); /* orig_size */ + READ_LE32(p, entry->crc32c); entry->ino = ino; @@ -214,6 +191,8 @@ int bfc_read_index(struct bfcfs_mount *bmp, struct thread *td) { uint8_t *index_buf = NULL; + uint8_t *p; + uint32_t version, count; size_t offset = 0; int error, i; @@ -232,33 +211,34 @@ bfc_read_index(struct bfcfs_mount *bmp, struct thread *td) error = bfc_pread(bmp, index_buf, bmp->footer.index_size, bmp->footer.index_offset, td); if (error) { - printf("bfcfs: failed to read index: %d\n", error); + BFCFS_ERR("failed to read index: %d", error); free(index_buf, M_BFCFS); return (error); } /* Parse index header (8 bytes: version + count) */ - if (bmp->footer.index_size < 8) { - printf("bfcfs: index too small for header\n"); + if (bmp->footer.index_size < BFC_INDEX_HEADER_SIZE) { + BFCFS_ERR("index too small for header"); free(index_buf, M_BFCFS); return (EINVAL); } - uint32_t version, count; - bcopy(index_buf, &version, 4); - version = LE32(version); - bcopy(index_buf + 4, &count, 4); - count = LE32(count); + /* Parse index header (version and count) */ + p = index_buf; + READ_LE32(p, version); + READ_LE32(p, count); - if (version != 1) { - printf("bfcfs: unsupported index version: %u\n", version); + if (version != BFC_INDEX_VERSION) { + BFCFS_ERR("unsupported index version: %u (expected %u)", + version, BFC_INDEX_VERSION); free(index_buf, M_BFCFS); return (EINVAL); } bmp->num_entries = count; - if (bmp->num_entries == 0 || bmp->num_entries > 1000000) { - printf("bfcfs: invalid entry count: %u\n", bmp->num_entries); + if (bmp->num_entries == 0 || bmp->num_entries > BFC_MAX_ENTRIES) { + BFCFS_ERR("invalid entry count: %u (max %u)", + bmp->num_entries, BFC_MAX_ENTRIES); free(index_buf, M_BFCFS); return (EINVAL); } @@ -269,13 +249,13 @@ bfc_read_index(struct bfcfs_mount *bmp, struct thread *td) bmp->index = malloc(bmp->num_entries * sizeof(struct bfcfs_index_entry), M_BFCFS, M_WAITOK | M_ZERO); - /* Parse all entries (starting after 8-byte header) */ - offset = 8; + /* Parse all entries (starting after index header) */ + offset = BFC_INDEX_HEADER_SIZE; for (i = 0; i < (int)bmp->num_entries; i++) { error = bfc_parse_index_entry(index_buf, bmp->footer.index_size, - &offset, &bmp->index[i], i + 1); /* inode numbers start at 1 */ + &offset, &bmp->index[i], i + BFC_INODE_START); if (error) { - printf("bfcfs: failed to parse index entry %d: %d\n", i, error); + BFCFS_ERR("failed to parse index entry %d: %d", i, error); goto fail; } diff --git a/bfc_io.c b/bfc_io.c index 64cee07..cb5c25c 100644 --- a/bfc_io.c +++ b/bfc_io.c @@ -78,9 +78,9 @@ bfc_crc32c(const void *buf, size_t len) { /* * calculate_crc32c() from FreeBSD already handles initialization and XOR - * Just pass 0 as initial CRC for a new calculation + * Just pass BFC_CRC32C_INIT as initial CRC for a new calculation */ - return calculate_crc32c(0, buf, len); + return calculate_crc32c(BFC_CRC32C_INIT, buf, len); } /* @@ -123,7 +123,7 @@ bfc_read_object(struct bfcfs_mount *bmp, struct bfcfs_index_entry *entry, /* Calculate content offset like Linux and BFC library do */ hdr_name_size = sizeof(struct bfc_obj_header) + le16toh(obj_hdr.name_len); - padding = ((hdr_name_size + 15) & ~15ULL) - hdr_name_size; /* BFC_ALIGN=16 */ + padding = BFC_ALIGN_UP(hdr_name_size, BFC_ALIGN) - hdr_name_size; content_offset = entry->obj_offset + hdr_name_size + padding; BFCFS_DEBUG("read_object: %s obj_off=%lu hdr=%zu name_len=%u hdr_name=%zu pad=%zu content_off=%ld", @@ -143,8 +143,8 @@ bfc_read_object(struct bfcfs_mount *bmp, struct bfcfs_index_entry *entry, if (bmp->verify_crc && offset == 0 && len == entry->size) { uint32_t crc = bfc_crc32c(buf, len); if (crc != entry->crc32c) { - printf("bfcfs: CRC mismatch for %s: " - "expected 0x%x, got 0x%x\n", + BFCFS_ERR("CRC mismatch for %s: " + "expected 0x%x, got 0x%x", entry->path, entry->crc32c, crc); return (EIO); } @@ -159,14 +159,14 @@ bfc_read_object(struct bfcfs_mount *bmp, struct bfcfs_index_entry *entry, * TODO: Add support for partial reads with decompression */ if (offset != 0 || len != entry->size) { - printf("bfcfs: partial reads not yet supported for " - "compressed/encrypted files\n"); + BFCFS_ERR("partial reads not yet supported for " + "compressed/encrypted files"); return (EOPNOTSUPP); } /* Handle compressed files */ if (entry->comp == BFC_COMP_ZSTD) { - printf("bfcfs: ZSTD decompression not yet implemented\n"); + BFCFS_ERR("ZSTD decompression not yet implemented"); return (EOPNOTSUPP); /* * TODO: Implement ZSTD decompression @@ -179,7 +179,7 @@ bfc_read_object(struct bfcfs_mount *bmp, struct bfcfs_index_entry *entry, /* Handle encrypted files */ if (entry->enc == BFC_ENC_CHACHA20_POLY1305) { - printf("bfcfs: ChaCha20-Poly1305 decryption not yet implemented\n"); + BFCFS_ERR("ChaCha20-Poly1305 decryption not yet implemented"); return (EOPNOTSUPP); /* * TODO: Implement decryption @@ -191,7 +191,7 @@ bfc_read_object(struct bfcfs_mount *bmp, struct bfcfs_index_entry *entry, } /* Unknown compression/encryption */ - printf("bfcfs: unsupported compression (%u) or encryption (%u)\n", + BFCFS_ERR("unsupported compression (%u) or encryption (%u)", entry->comp, entry->enc); return (EOPNOTSUPP); } diff --git a/bfcfs.h b/bfcfs.h index 08aff56..df0c973 100644 --- a/bfcfs.h +++ b/bfcfs.h @@ -31,6 +31,58 @@ #define BFC_FOOTER_SIZE 56 #define BFC_ALIGN 16 +/* Magic strings */ +#define BFC_MAGIC_STR "BFCFv1" +#define BFC_INDEX_MAGIC "BFCFIDX" +#define BFC_INDEX_END "BFCFEND" + +/* Sanity limits for container validation */ +#define BFC_MAX_BLOCK_SIZE (1024 * 1024) /* 1MB max block size */ +#define BFC_MAX_INDEX_SIZE (100 * 1024 * 1024) /* 100MB max index */ +#define BFC_MAX_ENTRIES 1000000 /* 1M max entries */ +#define BFC_MAX_PATH_LEN 4096 /* Maximum path length */ + +/* BFC index format constants */ +#define BFC_INDEX_VERSION 1 /* Supported index version */ +#define BFC_INDEX_HEADER_SIZE 8 /* Version (4) + count (4) */ +#define BFC_INDEX_ENTRY_METADATA_SIZE 48 /* Size of fixed metadata fields */ +#define BFC_PATH_LEN_SIZE 4 /* Size of path_len field (uint32_t) */ +#define BFC_INODE_START 1 /* First inode number (root is synthetic) */ + +/* Time conversion constants */ +#define NSEC_PER_SEC 1000000000ULL /* Nanoseconds per second */ + +/* Size alignment */ +#define BFC_BLOCK_ROUND 512 /* Round size to 512-byte blocks */ + +/* Alignment macros - use ULL suffix for 64-bit arithmetic with large file sizes */ +#define BFC_ALIGN_TO_BLOCK(size) (((size) + 511ULL) & ~511ULL) +#define BFC_ALIGN_UP(size, align) (((size) + ((align) - 1ULL)) & ~((align) - 1ULL)) + +/* Helper macros for reading little-endian fields */ +#define READ_LE32(ptr, field) do { \ + bcopy(ptr, &(field), 4); \ + (field) = LE32(field); \ + (ptr) += 4; \ +} while (0) + +#define READ_LE64(ptr, field) do { \ + bcopy(ptr, &(field), 8); \ + (field) = LE64(field); \ + (ptr) += 8; \ +} while (0) + +/* VFS constants */ +#define BFC_DEFAULT_IO_SIZE 65536 /* 64KB optimal I/O size */ +#define BFC_ROOT_MODE 0755 /* Synthetic root directory mode */ +#define BFC_NAME_MAX 255 /* Maximum filename length */ +#define BFC_PATH_MAX 1024 /* Maximum path length for pathconf */ +#define BFC_FILESIZEBITS 64 /* File size bits (64-bit offsets) */ +#define BFC_LINK_MAX 1 /* Max hard links (read-only FS) */ + +/* I/O constants */ +#define BFC_CRC32C_INIT 0 /* Initial CRC32C value */ + /* BFC object types */ #define BFC_TYPE_FILE 1 #define BFC_TYPE_DIR 2 @@ -208,7 +260,10 @@ int bfc_get_file_size(struct bfcfs_mount *bmp, off_t *size, struct thread *td); #define VFSTOBFCFS(mp) ((struct bfcfs_mount *)((mp)->mnt_data)) #define VTOBFCFS(vp) ((struct bfcfs_node *)((vp)->v_data)) -/* Debug printf */ +/* Logging macros */ +#define BFCFS_ERR(fmt, ...) printf("bfcfs: " fmt "\n", ##__VA_ARGS__) +#define BFCFS_WARN(fmt, ...) printf("bfcfs: " fmt "\n", ##__VA_ARGS__) + #ifdef DEBUG #define BFCFS_DEBUG(fmt, ...) printf("bfcfs: " fmt "\n", ##__VA_ARGS__) #else diff --git a/bfcfs_vfs.c b/bfcfs_vfs.c index 6cebbf0..bbac4ca 100644 --- a/bfcfs_vfs.c +++ b/bfcfs_vfs.c @@ -95,7 +95,7 @@ bfcfs_mount(struct mount *mp) /* Try to get the device path from "from" option */ error = vfs_getopt(mp->mnt_optnew, "from", &opt_ptr, &opt_len); if (error || opt_ptr == NULL) { - printf("bfcfs: mount requires device path or -o source=/path/to/file.bfc\n"); + BFCFS_ERR("mount requires device path or -o source=/path/to/file.bfc"); return (EINVAL); } } @@ -113,7 +113,7 @@ bfcfs_mount(struct mount *mp) error = vn_open(&nd, &flags, 0, NULL); if (error) { free(source_path, M_BFCFS); - printf("bfcfs: failed to open %s: error %d\n", source_path, error); + BFCFS_ERR("failed to open %s: error %d", source_path, error); return (error); } NDFREE_PNBUF(&nd); @@ -137,7 +137,8 @@ bfcfs_mount(struct mount *mp) LIST_INIT(&bmp->index_list); /* Parse optional mount options */ - bmp->verify_crc = 1; /* Default: verify CRC */ + bmp->verify_crc = 1; /* Default: verify CRC enabled */ + bmp->no_readahead = 0; /* Default: readahead enabled */ if (vfs_getopt(mp->mnt_optnew, "noverify", NULL, NULL) == 0) bmp->verify_crc = 0; if (vfs_getopt(mp->mnt_optnew, "noreadahead", NULL, NULL) == 0) @@ -146,14 +147,14 @@ bfcfs_mount(struct mount *mp) /* Read BFC header and validate */ error = bfc_read_header(bmp, td); if (error) { - printf("bfcfs: invalid BFC header: error %d\n", error); + BFCFS_ERR("invalid BFC header: error %d", error); goto fail; } /* Read and parse the index */ error = bfc_read_index(bmp, td); if (error) { - printf("bfcfs: failed to read index: error %d\n", error); + BFCFS_ERR("failed to read index: error %d", error); goto fail; } @@ -262,14 +263,14 @@ bfcfs_root(struct mount *mp, int flags, struct vnode **vpp) BFCFS_DEBUG("root: creating synthetic root directory"); error = bfcfs_vnode_create(bmp, NULL, vpp); if (error) { - printf("bfcfs: failed to create root vnode: %d\n", error); + BFCFS_ERR("failed to create root vnode: %d", error); return (error); } } else { /* Create/get vnode for explicit root entry */ error = bfcfs_vnode_create(bmp, root_entry, vpp); if (error) { - printf("bfcfs: failed to create root vnode: %d\n", error); + BFCFS_ERR("failed to create root vnode: %d", error); return (error); } } @@ -294,7 +295,7 @@ bfcfs_statfs(struct mount *mp, struct statfs *sbp) struct bfcfs_mount *bmp = VFSTOBFCFS(mp); sbp->f_bsize = bmp->header.block_size; - sbp->f_iosize = 65536; /* Optimal I/O size */ + sbp->f_iosize = BFC_DEFAULT_IO_SIZE; sbp->f_blocks = 0; /* Unknown (container is append-only) */ sbp->f_bfree = 0; /* Read-only */ sbp->f_bavail = 0; /* Read-only */ diff --git a/bfcfs_vnops.c b/bfcfs_vnops.c index 1d549fd..d644a5d 100644 --- a/bfcfs_vnops.c +++ b/bfcfs_vnops.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -198,7 +199,7 @@ bfcfs_getattr(struct vop_getattr_args *ap) /* Handle synthetic root (NULL entry) */ if (entry == NULL) { - vap->va_mode = S_IFDIR | 0755; + vap->va_mode = S_IFDIR | BFC_ROOT_MODE; vap->va_fileid = 1; vap->va_size = 0; vap->va_bytes = 0; @@ -212,9 +213,9 @@ bfcfs_getattr(struct vop_getattr_args *ap) vap->va_mode = entry->mode & ALLPERMS; vap->va_fileid = entry->ino; vap->va_size = entry->size; - vap->va_bytes = (entry->size + 511) & ~511ULL; - vap->va_atime.tv_sec = entry->mtime_ns / 1000000000ULL; - vap->va_atime.tv_nsec = entry->mtime_ns % 1000000000ULL; + vap->va_bytes = BFC_ALIGN_TO_BLOCK(entry->size); + vap->va_atime.tv_sec = entry->mtime_ns / NSEC_PER_SEC; + vap->va_atime.tv_nsec = entry->mtime_ns % NSEC_PER_SEC; vap->va_mtime = vap->va_atime; vap->va_ctime = vap->va_atime; vap->va_birthtime = vap->va_atime; @@ -262,7 +263,7 @@ bfcfs_access(struct vop_access_args *ap) return (EROFS); /* Get mode: synthetic root or from entry */ - mode = (node->entry == NULL) ? 0755 : (node->entry->mode & ALLPERMS); + mode = (node->entry == NULL) ? BFC_ROOT_MODE : (node->entry->mode & ALLPERMS); /* Simple permission check based on mode bits */ return (vaccess(vp->v_type, mode, 0, 0, accmode, ap->a_cred)); @@ -454,11 +455,15 @@ bfcfs_strategy(struct vop_strategy_args *ap) static int bfcfs_print(struct vop_print_args *ap) { +#ifdef DEBUG struct vnode *vp = ap->a_vp; struct bfcfs_node *node = VTOBFCFS(vp); - - printf(" vnode type=%d path=%s\n", + BFCFS_DEBUG("vnode type=%d path=%s", vp->v_type, node->entry ? node->entry->path : ""); +#else + (void)ap; +#endif + return (0); } @@ -471,20 +476,20 @@ bfcfs_pathconf(struct vop_pathconf_args *ap) int error = 0; switch (ap->a_name) { - case 1: /* _PC_LINK_MAX */ - *ap->a_retval = 1; + case _PC_LINK_MAX: + *ap->a_retval = BFC_LINK_MAX; break; - case 4: /* _PC_NAME_MAX */ - *ap->a_retval = 255; /* NAME_MAX */ + case _PC_NAME_MAX: + *ap->a_retval = BFC_NAME_MAX; break; - case 5: /* _PC_PATH_MAX */ - *ap->a_retval = 1024; /* PATH_MAX */ + case _PC_PATH_MAX: + *ap->a_retval = BFC_PATH_MAX; break; - case 7: /* _PC_NO_TRUNC */ - *ap->a_retval = 1; + case _PC_NO_TRUNC: + *ap->a_retval = 1; /* Filenames are never truncated */ break; - case 18: /* _PC_FILESIZEBITS */ - *ap->a_retval = 64; + case _PC_FILESIZEBITS: + *ap->a_retval = BFC_FILESIZEBITS; break; default: error = EINVAL; diff --git a/tests/Kyuafile b/tests/Kyuafile new file mode 100644 index 0000000..d0a0c33 --- /dev/null +++ b/tests/Kyuafile @@ -0,0 +1,11 @@ +-- BFCFS Test Suite Configuration +-- $FreeBSD$ + +syntax(2) + +test_suite("FreeBSD") + +atf_test_program{name="mount_test"} +atf_test_program{name="read_test"} +atf_test_program{name="readdir_test"} +atf_test_program{name="symlink_test"} diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..51b93f6 --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,49 @@ +# BFCFS Test Fixtures + +This directory contains BFC container files used for testing. + +## Creating Test Containers + +You need to create test BFC containers using the `bfc` tool from https://github.com/zombocoder/bfc: + +```bash +# Build and install bfc tool +git clone https://github.com/zombocoder/bfc.git +cd bfc +make +doas make install +cd .. + +# Create a simple test container +mkdir test_data +echo "Hello World" > test_data/index.html +echo "body { color: blue; }" > test_data/style.css +ln -s index.html test_data/link + +bfc create test.bfc ./test_data/* + +# Copy to fixtures directory +cp test.bfc /path/to/bfcfs-freebsd/tests/fixtures/ +``` + +## Required Test Containers + +The test suite expects the following containers: + +1. **test.bfc** - Basic container with: + - `index.html` - Simple HTML file + - `style.css` - Simple CSS file (optional) + - `link` - Symlink to index.html (optional) + +## Container Requirements + +- Containers must be valid BFC format with: + - Valid header and footer + - Valid index structure + - At least one file entry + - No compression/encryption (for basic tests) + +## GitHub Actions + +The CI workflow will create test containers automatically using the BFC tool +before running the tests. diff --git a/tests/fixtures/test.bfc b/tests/fixtures/test.bfc new file mode 100644 index 0000000..4ef1e51 Binary files /dev/null and b/tests/fixtures/test.bfc differ diff --git a/tests/h_funcs.subr b/tests/h_funcs.subr new file mode 100644 index 0000000..e9a989d --- /dev/null +++ b/tests/h_funcs.subr @@ -0,0 +1,110 @@ +#!/bin/sh +# +# Helper functions for BFCFS tests +# + +# Mount point for tests +Mount_Point=bfcfs_mnt + +# Fixtures directory +Fixtures_Dir=$(dirname $0)/fixtures + +# +# require_bfcfs +# +# Checks that bfcfs kernel module is available +# and can be loaded. Skips the test if not available. +# +require_bfcfs() { + atf_require_prog mount + atf_require_prog umount + atf_require_prog kldload + atf_require_prog kldunload + + # Check if module is already loaded + if kldstat -qm bfcfs; then + return 0 + fi + + # Try to load from common locations + local module_paths="/boot/modules/bfcfs.ko ../bfcfs.ko ./bfcfs.ko" + local loaded=0 + + for mod_path in $module_paths; do + if [ -f "$mod_path" ]; then + if kldload "$mod_path" 2>/dev/null; then + loaded=1 + break + fi + fi + done + + # If still not loaded, try system-wide kldload + if [ $loaded -eq 0 ]; then + if ! kldload bfcfs 2>/dev/null; then + atf_skip "bfcfs kernel module not available" + fi + fi + + # Verify module is loaded + if ! kldstat -qm bfcfs; then + atf_skip "bfcfs kernel module failed to load" + fi +} + +# +# test_mount container [options] +# +# Mounts a BFC container from the fixtures directory +# +test_mount() { + local container options container_path + container="${1}" + shift + options="$@" + + require_bfcfs + + # Create mount point + mkdir -p ${Mount_Point} || atf_fail "Cannot create mount point" + + # Get absolute path to container + container_path="${Fixtures_Dir}/${container}" + if [ ! -f "${container_path}" ]; then + atf_fail "Test container not found: ${container_path}" + fi + + # Always mount read-only (required by bfcfs) + # Add ro option if not already present + case "${options}" in + *ro*) ;; + *) options="-o ro ${options}" ;; + esac + + # Mount the BFC container + mount -t bfcfs ${options} ${container_path} ${Mount_Point} || \ + atf_fail "Cannot mount BFC container" +} + +# +# test_unmount +# +# Unmounts the test filesystem +# +test_unmount() { + umount ${Mount_Point} || atf_fail "Cannot unmount filesystem" + rmdir ${Mount_Point} 2>/dev/null || true +} + +# +# cleanup_mount +# +# Cleanup function to be called in test cleanup +# Forcefully unmounts if still mounted +# +cleanup_mount() { + if mount | grep -q ${Mount_Point}; then + umount -f ${Mount_Point} 2>/dev/null || true + fi + rmdir ${Mount_Point} 2>/dev/null || true +} diff --git a/tests/mount_test b/tests/mount_test new file mode 100755 index 0000000..600f6ea --- /dev/null +++ b/tests/mount_test @@ -0,0 +1,84 @@ +#!/usr/libexec/atf-sh +# +# BFCFS mount/unmount tests +# + +. $(dirname $0)/h_funcs.subr + +atf_test_case plain cleanup +plain_head() { + atf_set "descr" "Tests basic mount and unmount of BFC container" + atf_set "require.user" "root" +} +plain_body() { + test_mount test.bfc + # Verify mount point exists and is accessible + test -d ${Mount_Point} || atf_fail "Mount point is not a directory" + test_unmount +} +plain_cleanup() { + cleanup_mount +} + +atf_test_case verify_readonly cleanup +verify_readonly_head() { + atf_set "descr" "Verifies that BFCFS is mounted read-only" + atf_set "require.user" "root" +} +verify_readonly_body() { + test_mount test.bfc + # Check that filesystem is read-only + mount | grep ${Mount_Point} | grep -q "read-only" || \ + atf_fail "Filesystem is not mounted read-only" + + # Try to create a file (should fail) + if touch ${Mount_Point}/testfile 2>/dev/null; then + atf_fail "Should not be able to create files on read-only filesystem" + fi + test_unmount +} +verify_readonly_cleanup() { + cleanup_mount +} + +atf_test_case mount_options cleanup +mount_options_head() { + atf_set "descr" "Tests mount with noverify option" + atf_set "require.user" "root" +} +mount_options_body() { + test_mount test.bfc -o noverify + mount | grep ${Mount_Point} || atf_fail "Filesystem not mounted" + test_unmount +} +mount_options_cleanup() { + cleanup_mount +} + +atf_test_case invalid_container cleanup +invalid_container_head() { + atf_set "descr" "Tests mounting invalid BFC container fails gracefully" + atf_set "require.user" "root" +} +invalid_container_body() { + require_bfcfs + mkdir -p ${Mount_Point} + + # Try to mount non-existent file (should fail) + if mount -t bfcfs /nonexistent.bfc ${Mount_Point} 2>/dev/null; then + umount ${Mount_Point} + atf_fail "Should not be able to mount non-existent container" + fi + + rmdir ${Mount_Point} +} +invalid_container_cleanup() { + cleanup_mount +} + +atf_init_test_cases() { + atf_add_test_case plain + atf_add_test_case verify_readonly + atf_add_test_case mount_options + atf_add_test_case invalid_container +} diff --git a/tests/read_test b/tests/read_test new file mode 100755 index 0000000..2b4f04f --- /dev/null +++ b/tests/read_test @@ -0,0 +1,106 @@ +#!/usr/libexec/atf-sh +# +# BFCFS read operations tests +# + +. $(dirname $0)/h_funcs.subr + +atf_test_case read_file cleanup +read_file_head() { + atf_set "descr" "Tests reading a file from BFC container" + atf_set "require.user" "root" +} +read_file_body() { + test_mount test.bfc + + # Test reading index.html + test -f ${Mount_Point}/index.html || \ + atf_fail "index.html not found in container" + + # Read the file + content=$(cat ${Mount_Point}/index.html 2>/dev/null) || \ + atf_fail "Cannot read index.html" + + # Check that we got some content + test -n "$content" || atf_fail "File is empty" + + test_unmount +} +read_file_cleanup() { + cleanup_mount +} + +atf_test_case read_multiple cleanup +read_multiple_head() { + atf_set "descr" "Tests reading multiple files sequentially" + atf_set "require.user" "root" +} +read_multiple_body() { + test_mount test.bfc + + # Read multiple files + for file in index.html style.css; do + if [ -f ${Mount_Point}/${file} ]; then + cat ${Mount_Point}/${file} >/dev/null || \ + atf_fail "Cannot read ${file}" + fi + done + + test_unmount +} +read_multiple_cleanup() { + cleanup_mount +} + +atf_test_case read_symlink cleanup +read_symlink_head() { + atf_set "descr" "Tests reading file through symlink" + atf_set "require.user" "root" +} +read_symlink_body() { + test_mount test.bfc + + # Check if symlink exists + if [ -L ${Mount_Point}/link ]; then + # Try to read through symlink + cat ${Mount_Point}/link >/dev/null || \ + atf_fail "Cannot read through symlink" + else + atf_skip "No symlink in test container" + fi + + test_unmount +} +read_symlink_cleanup() { + cleanup_mount +} + +atf_test_case stat_file cleanup +stat_file_head() { + atf_set "descr" "Tests stat(2) on files in BFC container" + atf_set "require.user" "root" +} +stat_file_body() { + test_mount test.bfc + + # Stat a file + eval $(stat -s ${Mount_Point}/index.html 2>/dev/null) || \ + atf_fail "Cannot stat index.html" + + # Check that we got valid stat info + test -n "$st_size" || atf_fail "File size is not set" + test "$st_size" -gt 0 || atf_fail "File size is zero" + test -n "$st_mode" || atf_fail "File mode is not set" + + test_unmount +} +stat_file_cleanup() { + cleanup_mount +} + +atf_init_test_cases() { + atf_add_test_case read_file + atf_add_test_case read_multiple + atf_add_test_case read_symlink + atf_add_test_case stat_file +} diff --git a/tests/readdir_test b/tests/readdir_test new file mode 100755 index 0000000..a668b2f --- /dev/null +++ b/tests/readdir_test @@ -0,0 +1,101 @@ +#!/usr/libexec/atf-sh +# +# BFCFS directory listing tests +# + +. $(dirname $0)/h_funcs.subr + +atf_test_case list_root cleanup +list_root_head() { + atf_set "descr" "Tests listing root directory" + atf_set "require.user" "root" +} +list_root_body() { + test_mount test.bfc + + # List root directory + ls ${Mount_Point} >/dev/null || \ + atf_fail "Cannot list root directory" + + # Count entries + count=$(ls -1 ${Mount_Point} | wc -l) + test "$count" -gt 0 || atf_fail "Root directory is empty" + + test_unmount +} +list_root_cleanup() { + cleanup_mount +} + +atf_test_case list_details cleanup +list_details_head() { + atf_set "descr" "Tests listing with details (ls -l)" + atf_set "require.user" "root" +} +list_details_body() { + test_mount test.bfc + + # List with details + ls -l ${Mount_Point} >/dev/null || \ + atf_fail "Cannot list directory with details" + + # Check that we can see file sizes + output=$(ls -l ${Mount_Point}/index.html 2>/dev/null) + echo "$output" | grep -q "index.html" || \ + atf_fail "File not listed correctly" + + test_unmount +} +list_details_cleanup() { + cleanup_mount +} + +atf_test_case list_all cleanup +list_all_head() { + atf_set "descr" "Tests listing with hidden files (ls -a)" + atf_set "require.user" "root" +} +list_all_body() { + test_mount test.bfc + + # List all files (note: BFCFS currently doesn't return . and .. entries) + # This is acceptable behavior for a read-only filesystem + output=$(ls -a ${Mount_Point}) + + # At minimum, verify we get some files + test -n "$output" || atf_fail "No files listed" + + test_unmount +} +list_all_cleanup() { + cleanup_mount +} + +atf_test_case find_files cleanup +find_files_head() { + atf_set "descr" "Tests recursive file finding" + atf_set "require.user" "root" +} +find_files_body() { + test_mount test.bfc + + # Use find to list all files + find ${Mount_Point} -type f >/dev/null || \ + atf_fail "Cannot use find on mounted filesystem" + + # Count files + count=$(find ${Mount_Point} -type f | wc -l) + test "$count" -gt 0 || atf_fail "No files found" + + test_unmount +} +find_files_cleanup() { + cleanup_mount +} + +atf_init_test_cases() { + atf_add_test_case list_root + atf_add_test_case list_details + atf_add_test_case list_all + atf_add_test_case find_files +} diff --git a/tests/symlink_test b/tests/symlink_test new file mode 100755 index 0000000..776f813 --- /dev/null +++ b/tests/symlink_test @@ -0,0 +1,92 @@ +#!/usr/libexec/atf-sh +# +# BFCFS symlink tests +# + +. $(dirname $0)/h_funcs.subr + +atf_test_case readlink cleanup +readlink_head() { + atf_set "descr" "Tests reading symlink target" + atf_set "require.user" "root" +} +readlink_body() { + test_mount test.bfc + + # Check if test container has symlinks + if ! find ${Mount_Point} -type l 2>/dev/null | grep -q .; then + test_unmount + atf_skip "No symlinks in test container" + fi + + # Find a symlink and read it + link=$(find ${Mount_Point} -type l | head -1) + target=$(readlink "$link") || \ + atf_fail "Cannot read symlink" + + test -n "$target" || atf_fail "Symlink target is empty" + + test_unmount +} +readlink_cleanup() { + cleanup_mount +} + +atf_test_case follow_symlink cleanup +follow_symlink_head() { + atf_set "descr" "Tests following symlinks to read target file" + atf_set "require.user" "root" +} +follow_symlink_body() { + test_mount test.bfc + + # Check if test container has symlinks + if ! find ${Mount_Point} -type l 2>/dev/null | grep -q .; then + test_unmount + atf_skip "No symlinks in test container" + fi + + # Find a symlink and read through it + link=$(find ${Mount_Point} -type l | head -1) + cat "$link" >/dev/null 2>&1 || \ + atf_fail "Cannot read through symlink" + + test_unmount +} +follow_symlink_cleanup() { + cleanup_mount +} + +atf_test_case stat_symlink cleanup +stat_symlink_head() { + atf_set "descr" "Tests stat on symlinks (lstat)" + atf_set "require.user" "root" +} +stat_symlink_body() { + test_mount test.bfc + + # Check if test container has symlinks + if ! find ${Mount_Point} -type l 2>/dev/null | grep -q .; then + test_unmount + atf_skip "No symlinks in test container" + fi + + # Find a symlink and stat it + link=$(find ${Mount_Point} -type l | head -1) + eval $(stat -s "$link") || \ + atf_fail "Cannot stat symlink" + + # Check if it's recognized as a symlink + test -L "$link" || atf_fail "File not recognized as symlink" + + test_unmount +} +stat_symlink_cleanup() { + cleanup_mount +} + +atf_init_test_cases() { + atf_add_test_case readlink + atf_add_test_case follow_symlink + atf_add_test_case stat_symlink +}