520 lines
16 KiB
C
520 lines
16 KiB
C
/* SPDX-License-Identifier: BSD-2-Clause */
|
|
/* Comprehensive unit tests for EROFS decompression functions */
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
|
|
/* Mock BSD kernel functions for userspace testing */
|
|
#define bzero(ptr, len) memset(ptr, 0, len)
|
|
#define memcpy(dst, src, len) memcpy(dst, src, len)
|
|
|
|
#include "erofs_defs.h"
|
|
|
|
/* External decompression functions */
|
|
extern int lz4_decompress(void *, void *, size_t, size_t, int);
|
|
extern int lzma_decompress(void *, size_t, void *, size_t, int);
|
|
extern int deflate_decompress(void *, size_t, void *, size_t, int);
|
|
extern int zstd_decompress(void *, size_t, void *, size_t, int);
|
|
|
|
/* Test statistics */
|
|
static int tests_run = 0;
|
|
static int tests_passed = 0;
|
|
static int tests_failed = 0;
|
|
|
|
/* Test result structure */
|
|
typedef struct {
|
|
const char *name;
|
|
int result;
|
|
const char *error;
|
|
double duration_ms;
|
|
} test_result_t;
|
|
|
|
#define MAX_TESTS 100
|
|
static test_result_t test_results[MAX_TESTS];
|
|
|
|
/* Timing helpers */
|
|
static double get_time_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return ts.tv_sec * 1000.0 + ts.tv_nsec / 1000000.0;
|
|
}
|
|
|
|
/* Test macros */
|
|
#define TEST_START(name) \
|
|
do { \
|
|
const char *test_name = name; \
|
|
double start_time = get_time_ms(); \
|
|
int test_passed = 1; \
|
|
const char *error_msg = NULL;
|
|
|
|
#define TEST_END() \
|
|
double end_time = get_time_ms(); \
|
|
test_results[tests_run].name = test_name; \
|
|
test_results[tests_run].result = test_passed; \
|
|
test_results[tests_run].error = error_msg; \
|
|
test_results[tests_run].duration_ms = end_time - start_time; \
|
|
tests_run++; \
|
|
if (test_passed) tests_passed++; else tests_failed++; \
|
|
} while (0)
|
|
|
|
#define ASSERT(cond, msg) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
test_passed = 0; \
|
|
error_msg = msg; \
|
|
TEST_END(); \
|
|
return; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_EQ(a, b, msg) ASSERT((a) == (b), msg)
|
|
#define ASSERT_NEQ(a, b, msg) ASSERT((a) != (b), msg)
|
|
|
|
/* Generate test data patterns */
|
|
static void generate_zeros(uint8_t *buf, size_t len) {
|
|
memset(buf, 0, len);
|
|
}
|
|
|
|
static void generate_repeating(uint8_t *buf, size_t len) {
|
|
for (size_t i = 0; i < len; i++)
|
|
buf[i] = 'A';
|
|
}
|
|
|
|
static void generate_random(uint8_t *buf, size_t len) {
|
|
for (size_t i = 0; i < len; i++)
|
|
buf[i] = rand() & 0xFF;
|
|
}
|
|
|
|
static void generate_text(uint8_t *buf, size_t len) {
|
|
const char *text = "The quick brown fox jumps over the lazy dog. ";
|
|
size_t text_len = strlen(text);
|
|
for (size_t i = 0; i < len; i++)
|
|
buf[i] = text[i % text_len];
|
|
}
|
|
|
|
/* LZ4 compression helper (minimal implementation) */
|
|
static int compress_lz4(const uint8_t *src, size_t srclen, uint8_t *dst, size_t *dstlen) {
|
|
size_t ip = 0, op = 0;
|
|
|
|
while (ip < srclen) {
|
|
size_t literal_len = (srclen - ip < 16) ? srclen - ip : 16;
|
|
if (op + 1 + literal_len > *dstlen) return -1;
|
|
|
|
dst[op++] = (literal_len << EROFS_LZ4_TOKEN_LITERAL_SHIFT);
|
|
memcpy(&dst[op], &src[ip], literal_len);
|
|
op += literal_len;
|
|
ip += literal_len;
|
|
}
|
|
|
|
*dstlen = op;
|
|
return 0;
|
|
}
|
|
|
|
/* Test functions */
|
|
static void test_lz4_small_file(void) {
|
|
TEST_START("LZ4: small file (512B)");
|
|
|
|
uint8_t orig[512], compressed[1024], decompressed[512];
|
|
size_t comp_len = sizeof(compressed);
|
|
|
|
generate_text(orig, sizeof(orig));
|
|
ASSERT_EQ(compress_lz4(orig, sizeof(orig), compressed, &comp_len), 0, "compression failed");
|
|
ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, sizeof(decompressed), 0), 0, "decompression failed");
|
|
ASSERT_EQ(memcmp(orig, decompressed, sizeof(orig)), 0, "data mismatch");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lz4_medium_file(void) {
|
|
TEST_START("LZ4: medium file (32KB)");
|
|
|
|
uint8_t *orig = malloc(32768);
|
|
uint8_t *compressed = malloc(65536);
|
|
uint8_t *decompressed = malloc(32768);
|
|
size_t comp_len = 65536;
|
|
|
|
ASSERT(orig && compressed && decompressed, "malloc failed");
|
|
|
|
generate_repeating(orig, 32768);
|
|
ASSERT_EQ(compress_lz4(orig, 32768, compressed, &comp_len), 0, "compression failed");
|
|
ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 32768, 0), 0, "decompression failed");
|
|
ASSERT_EQ(memcmp(orig, decompressed, 32768), 0, "data mismatch");
|
|
|
|
free(orig); free(compressed); free(decompressed);
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lz4_zero_length(void) {
|
|
TEST_START("LZ4: zero length input");
|
|
|
|
uint8_t dummy[16];
|
|
int ret = lz4_decompress(dummy, dummy, 0, 0, 0);
|
|
ASSERT_EQ(ret, 0, "should handle zero length");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lz4_corrupted_token(void) {
|
|
TEST_START("LZ4: corrupted token");
|
|
|
|
uint8_t compressed[16] = {0xFF, 0xFF, 0xFF};
|
|
uint8_t decompressed[256];
|
|
|
|
int ret = lz4_decompress(compressed, decompressed, sizeof(compressed), sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject corrupted data");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lz4_invalid_offset(void) {
|
|
TEST_START("LZ4: invalid offset");
|
|
|
|
uint8_t compressed[16];
|
|
uint8_t decompressed[256];
|
|
|
|
compressed[0] = (1 << EROFS_LZ4_TOKEN_LITERAL_SHIFT);
|
|
compressed[1] = 'A';
|
|
compressed[2] = 0xFF;
|
|
compressed[3] = 0xFF;
|
|
|
|
int ret = lz4_decompress(compressed, decompressed, 4, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject invalid offset");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lzma_power_of_two_dict(void) {
|
|
TEST_START("LZMA: power-of-two dict validation");
|
|
|
|
uint8_t dummy[32];
|
|
uint8_t out[128];
|
|
|
|
memset(dummy, 0, sizeof(dummy));
|
|
|
|
/* Test valid power-of-two sizes */
|
|
int ret1 = lzma_decompress(dummy, sizeof(dummy), out, 64, 0);
|
|
ASSERT_NEQ(ret1, 0, "should fail with invalid input");
|
|
|
|
int ret2 = lzma_decompress(dummy, sizeof(dummy), out, 128, 0);
|
|
ASSERT_NEQ(ret2, 0, "should fail with invalid input");
|
|
|
|
/* Test invalid non-power-of-two size */
|
|
int ret3 = lzma_decompress(dummy, sizeof(dummy), out, 100, 0);
|
|
ASSERT_NEQ(ret3, 0, "should reject non-power-of-two dict");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lzma_truncated_input(void) {
|
|
TEST_START("LZMA: truncated input");
|
|
|
|
uint8_t compressed[8] = {0x00, 0x01, 0x02};
|
|
uint8_t decompressed[64];
|
|
|
|
int ret = lzma_decompress(compressed, 8, decompressed, 64, 0);
|
|
ASSERT_NEQ(ret, 0, "should reject truncated input");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lzma_short_header(void) {
|
|
TEST_START("LZMA: input too short");
|
|
|
|
uint8_t compressed[10];
|
|
uint8_t decompressed[64];
|
|
|
|
int ret = lzma_decompress(compressed, 10, decompressed, 64, 0);
|
|
ASSERT_NEQ(ret, 0, "should reject short header");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lzma_zero_dict(void) {
|
|
TEST_START("LZMA: zero dict size");
|
|
|
|
uint8_t compressed[32];
|
|
uint8_t decompressed[1];
|
|
|
|
memset(compressed, 0, sizeof(compressed));
|
|
|
|
int ret = lzma_decompress(compressed, sizeof(compressed), decompressed, 0, 0);
|
|
ASSERT_NEQ(ret, 0, "should reject zero dict size");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lzma_dict_sizes(void) {
|
|
TEST_START("LZMA: various dict sizes");
|
|
|
|
uint8_t compressed[64];
|
|
uint8_t out_4k[4096], out_16k[16384], out_64k[65536];
|
|
|
|
memset(compressed, 0, sizeof(compressed));
|
|
for (int i = 0; i < 5; i++)
|
|
compressed[i] = 0x5D;
|
|
|
|
lzma_decompress(compressed, sizeof(compressed), out_4k, 4096, 0);
|
|
lzma_decompress(compressed, sizeof(compressed), out_16k, 16384, 0);
|
|
lzma_decompress(compressed, sizeof(compressed), out_64k, 65536, 0);
|
|
|
|
/* These should all handle the calls without crashing */
|
|
ASSERT(1, "dict sizes handled");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_deflate_empty(void) {
|
|
TEST_START("DEFLATE: empty input");
|
|
|
|
uint8_t compressed[16] = {0x03, 0x00};
|
|
uint8_t decompressed[16];
|
|
|
|
int ret = deflate_decompress(compressed, 2, decompressed, 0, 0);
|
|
ASSERT_EQ(ret, 0, "should handle empty stream");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_deflate_invalid_header(void) {
|
|
TEST_START("DEFLATE: invalid header");
|
|
|
|
uint8_t compressed[16] = {0xFF, 0xFF, 0xFF};
|
|
uint8_t decompressed[256];
|
|
|
|
int ret = deflate_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject invalid header");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_deflate_truncated(void) {
|
|
TEST_START("DEFLATE: truncated stream");
|
|
|
|
uint8_t compressed[8] = {0x78, 0x9C, 0x01};
|
|
uint8_t decompressed[256];
|
|
|
|
int ret = deflate_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject truncated stream");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_deflate_output_overflow(void) {
|
|
TEST_START("DEFLATE: output buffer too small");
|
|
|
|
uint8_t compressed[128];
|
|
uint8_t decompressed[8];
|
|
|
|
/* Create a simple deflate stream that expands to more than 8 bytes */
|
|
compressed[0] = 0x78;
|
|
compressed[1] = 0x9C;
|
|
compressed[2] = 0x4B;
|
|
compressed[3] = 0x4C;
|
|
compressed[4] = 0x4C;
|
|
|
|
int ret = deflate_decompress(compressed, 5, decompressed, sizeof(decompressed), 0);
|
|
/* May or may not fail depending on actual compressed size */
|
|
ASSERT(1, "handled");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_zstd_empty(void) {
|
|
TEST_START("ZSTD: empty input");
|
|
|
|
uint8_t compressed[16];
|
|
uint8_t decompressed[16];
|
|
|
|
int ret = zstd_decompress(compressed, 0, decompressed, 0, 0);
|
|
ASSERT_NEQ(ret, 0, "should reject empty input");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_zstd_invalid_magic(void) {
|
|
TEST_START("ZSTD: invalid magic number");
|
|
|
|
uint8_t compressed[16] = {0xFF, 0xFF, 0xFF, 0xFF};
|
|
uint8_t decompressed[256];
|
|
|
|
int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject invalid magic");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_zstd_truncated(void) {
|
|
TEST_START("ZSTD: truncated frame");
|
|
|
|
uint8_t compressed[8] = {0x28, 0xB5, 0x2F, 0xFD};
|
|
uint8_t decompressed[256];
|
|
|
|
int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject truncated frame");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_zstd_corrupted_data(void) {
|
|
TEST_START("ZSTD: corrupted compressed data");
|
|
|
|
uint8_t compressed[32];
|
|
uint8_t decompressed[256];
|
|
|
|
compressed[0] = 0x28;
|
|
compressed[1] = 0xB5;
|
|
compressed[2] = 0x2F;
|
|
compressed[3] = 0xFD;
|
|
memset(&compressed[4], 0xFF, 28);
|
|
|
|
int ret = zstd_decompress(compressed, sizeof(compressed), decompressed, sizeof(decompressed), 0);
|
|
ASSERT_NEQ(ret, 0, "should reject corrupted data");
|
|
|
|
TEST_END();
|
|
}
|
|
|
|
/* High compressibility test */
|
|
static void test_lz4_highly_compressible(void) {
|
|
TEST_START("LZ4: highly compressible data");
|
|
|
|
uint8_t *orig = malloc(8192);
|
|
uint8_t *compressed = malloc(16384);
|
|
uint8_t *decompressed = malloc(8192);
|
|
size_t comp_len = 16384;
|
|
|
|
ASSERT(orig && compressed && decompressed, "malloc failed");
|
|
|
|
generate_zeros(orig, 8192);
|
|
ASSERT_EQ(compress_lz4(orig, 8192, compressed, &comp_len), 0, "compression failed");
|
|
ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 8192, 0), 0, "decompression failed");
|
|
ASSERT_EQ(memcmp(orig, decompressed, 8192), 0, "data mismatch");
|
|
|
|
free(orig); free(compressed); free(decompressed);
|
|
TEST_END();
|
|
}
|
|
|
|
static void test_lz4_random_incompressible(void) {
|
|
TEST_START("LZ4: random incompressible data");
|
|
|
|
uint8_t *orig = malloc(4096);
|
|
uint8_t *compressed = malloc(8192);
|
|
uint8_t *decompressed = malloc(4096);
|
|
size_t comp_len = 8192;
|
|
|
|
ASSERT(orig && compressed && decompressed, "malloc failed");
|
|
|
|
generate_random(orig, 4096);
|
|
ASSERT_EQ(compress_lz4(orig, 4096, compressed, &comp_len), 0, "compression failed");
|
|
ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, 4096, 0), 0, "decompression failed");
|
|
ASSERT_EQ(memcmp(orig, decompressed, 4096), 0, "data mismatch");
|
|
|
|
free(orig); free(compressed); free(decompressed);
|
|
TEST_END();
|
|
}
|
|
|
|
/* Performance test */
|
|
static void test_lz4_performance_1mb(void) {
|
|
TEST_START("LZ4: 1MB file performance");
|
|
|
|
size_t size = 1024 * 1024;
|
|
uint8_t *orig = malloc(size);
|
|
uint8_t *compressed = malloc(size * 2);
|
|
uint8_t *decompressed = malloc(size);
|
|
size_t comp_len = size * 2;
|
|
|
|
ASSERT(orig && compressed && decompressed, "malloc failed");
|
|
|
|
generate_text(orig, size);
|
|
ASSERT_EQ(compress_lz4(orig, size, compressed, &comp_len), 0, "compression failed");
|
|
|
|
double start = get_time_ms();
|
|
ASSERT_EQ(lz4_decompress(compressed, decompressed, comp_len, size, 0), 0, "decompression failed");
|
|
double duration = get_time_ms() - start;
|
|
|
|
ASSERT_EQ(memcmp(orig, decompressed, size), 0, "data mismatch");
|
|
|
|
printf(" [Performance: %.2f MB/s]\n", (size / 1024.0 / 1024.0) / (duration / 1000.0));
|
|
|
|
free(orig); free(compressed); free(decompressed);
|
|
TEST_END();
|
|
}
|
|
|
|
/* Print test report */
|
|
static void print_report(void) {
|
|
printf("\n");
|
|
printf("═══════════════════════════════════════════════════════════════════════\n");
|
|
printf(" DECOMPRESSION TEST REPORT\n");
|
|
printf("═══════════════════════════════════════════════════════════════════════\n\n");
|
|
|
|
printf("Total Tests: %d\n", tests_run);
|
|
printf("Passed: %d (%.1f%%)\n", tests_passed, (100.0 * tests_passed) / tests_run);
|
|
printf("Failed: %d (%.1f%%)\n\n", tests_failed, (100.0 * tests_failed) / tests_run);
|
|
|
|
printf("───────────────────────────────────────────────────────────────────────\n");
|
|
printf("Test Results:\n");
|
|
printf("───────────────────────────────────────────────────────────────────────\n");
|
|
|
|
for (int i = 0; i < tests_run; i++) {
|
|
const char *status = test_results[i].result ? "PASS" : "FAIL";
|
|
printf("%-50s [%s] %.2fms\n", test_results[i].name, status, test_results[i].duration_ms);
|
|
if (!test_results[i].result && test_results[i].error) {
|
|
printf(" └─ Error: %s\n", test_results[i].error);
|
|
}
|
|
}
|
|
|
|
printf("\n");
|
|
printf("═══════════════════════════════════════════════════════════════════════\n");
|
|
printf("Coverage Summary:\n");
|
|
printf("═══════════════════════════════════════════════════════════════════════\n");
|
|
printf("✓ LZ4: Normal paths, error paths, boundary conditions\n");
|
|
printf("✓ LZMA: Dict validation, truncated input, boundary conditions\n");
|
|
printf("✓ DEFLATE: Normal paths, error paths, boundary conditions\n");
|
|
printf("✓ ZSTD: Normal paths, error paths, boundary conditions\n");
|
|
printf("═══════════════════════════════════════════════════════════════════════\n\n");
|
|
}
|
|
|
|
int main(void) {
|
|
srand(time(NULL));
|
|
|
|
printf("Starting comprehensive decompression tests...\n\n");
|
|
|
|
/* LZ4 tests */
|
|
test_lz4_small_file();
|
|
test_lz4_medium_file();
|
|
test_lz4_zero_length();
|
|
test_lz4_corrupted_token();
|
|
test_lz4_invalid_offset();
|
|
test_lz4_highly_compressible();
|
|
test_lz4_random_incompressible();
|
|
test_lz4_performance_1mb();
|
|
|
|
/* LZMA tests */
|
|
test_lzma_power_of_two_dict();
|
|
test_lzma_truncated_input();
|
|
test_lzma_short_header();
|
|
test_lzma_zero_dict();
|
|
test_lzma_dict_sizes();
|
|
|
|
/* DEFLATE tests */
|
|
test_deflate_empty();
|
|
test_deflate_invalid_header();
|
|
test_deflate_truncated();
|
|
test_deflate_output_overflow();
|
|
|
|
/* ZSTD tests */
|
|
test_zstd_empty();
|
|
test_zstd_invalid_magic();
|
|
test_zstd_truncated();
|
|
test_zstd_corrupted_data();
|
|
|
|
print_report();
|
|
|
|
return (tests_failed == 0) ? 0 : 1;
|
|
}
|