From a30c8cf694ac573090cbbf8286c1b3e0909aa8a4 Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Thu, 12 Mar 2026 02:04:13 -0400 Subject: [PATCH] Add archive creation with SuperMaster compression CLI: uc2 -w [-L level] archive.uc2 files... Creates UC2 archives with long filename tags and the built-in 49KB SuperMaster dictionary for improved compression via LZ77 prefix matching. Library: uc2_compress_ex() accepts master data to pre-fill the sliding window and hash chains. uc2_get_supermaster() decompresses the embedded super.bin. uc2_compress() unchanged (backward compatible, NoMaster). Tests: 5 SuperMaster roundtrip tests, CLI create/extract CTest script. --- cli/src/main.c | 285 +++++++++++++++++++++++++++++++++++- docs/library.rst | 15 ++ docs/quickstart.rst | 2 + docs/usage.rst | 11 ++ lib/include/uc2/libuc2.h | 21 +++ lib/src/compress.c | 30 +++- lib/src/decompress.c | 24 +++ tests/CMakeLists.txt | 8 + tests/src/test_roundtrip.c | 65 ++++++-- tests/test_cli_create.cmake | 63 ++++++++ 10 files changed, 511 insertions(+), 13 deletions(-) create mode 100644 tests/test_cli_create.cmake diff --git a/cli/src/main.c b/cli/src/main.c index 412b992..a8f8d84 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -44,14 +44,16 @@ struct options { bool all:1; bool test:1; bool pipe:1; + bool create:1; bool overwrite:1; bool no_dir_meta:1; bool no_file_meta:1; bool help:1; char sep; + int level; char *archive; char *dest; -} opt = {.sep = ' '}; +} opt = {.sep = ' ', .level = 4}; static int my_read(void *ctx, unsigned pos, void *ptr, unsigned len) { @@ -469,6 +471,268 @@ static bool extract_cb(struct node *ne, void *ctx, enum cause cause) return true; } +/* --- Archive creation --- */ + +static void w16(unsigned char *p, unsigned v) +{ + p[0] = v & 0xFF; + p[1] = (v >> 8) & 0xFF; +} + +static void w32(unsigned char *p, unsigned v) +{ + p[0] = v & 0xFF; + p[1] = (v >> 8) & 0xFF; + p[2] = (v >> 16) & 0xFF; + p[3] = (v >> 24) & 0xFF; +} + +static unsigned short fletcher_csum(const unsigned char *data, unsigned len) +{ + if (!len) return 0xA55A; + unsigned v = 0xA55A; + const unsigned char *p = data; + const unsigned char *e = p + len - 1; + if (v > 0xFFFF) + v ^= *p++ << 8; + while (p < e) { + v ^= p[0] | p[1] << 8; + p += 2; + } + v &= 0xFFFF; + if (p == e) + v ^= *p | 0x10000; + return (unsigned short)(v & 0xFFFF); +} + +static unsigned to_dos_time(time_t t) +{ + struct tm *tm = localtime(&t); + if (!tm || tm->tm_year < 80) return 0; + return ((unsigned)(tm->tm_year - 80) << 25) | + ((unsigned)(tm->tm_mon + 1) << 21) | + ((unsigned)tm->tm_mday << 16) | + ((unsigned)tm->tm_hour << 11) | + ((unsigned)tm->tm_min << 5) | + ((unsigned)(tm->tm_sec / 2)); +} + +static void make_dos_name(unsigned char dos_name[11], const char *filename) +{ + const char *base = strrchr(filename, '/'); + base = base ? base + 1 : filename; + const char *dot = strrchr(base, '.'); + int namelen = dot ? (int)(dot - base) : (int)strlen(base); + + memset(dos_name, ' ', 11); + for (int i = 0; i < 8 && i < namelen; i++) + dos_name[i] = toupper((unsigned char)base[i]); + if (dot) { + const char *ext = dot + 1; + for (int i = 0; i < 3 && ext[i]; i++) + dos_name[8 + i] = toupper((unsigned char)ext[i]); + } +} + +struct mem_reader { + const unsigned char *data; + unsigned pos, len; +}; + +static int mem_read_cb(void *ctx, void *buf, unsigned len) +{ + struct mem_reader *mr = ctx; + unsigned avail = mr->len - mr->pos; + if (len > avail) len = avail; + if (len > 0) { + memcpy(buf, mr->data + mr->pos, len); + mr->pos += len; + } + return (int)len; +} + +static int fread_cb(void *ctx, void *buf, unsigned len) +{ + return (int)fread(buf, 1, len, (FILE *)ctx); +} + +static int fwrite_cb(void *ctx, const void *ptr, unsigned len) +{ + return fwrite(ptr, 1, len, (FILE *)ctx) == len ? 0 : -1; +} + +struct file_rec { + char path[PATH_MAX]; + char name[300]; + unsigned char dos_name[11]; + unsigned size; + unsigned csize; + unsigned short csum; + unsigned offset; + unsigned dos_time; +}; + +static int create_archive(int nfiles, char **files) +{ + struct file_rec *recs = calloc(nfiles, sizeof *recs); + if (!recs) + err(EXIT_FAILURE, "malloc"); + + for (int i = 0; i < nfiles; i++) { + struct stat st; + if (stat(files[i], &st) < 0) + err(EXIT_FAILURE, "%s", files[i]); + if (!S_ISREG(st.st_mode)) + errx(EXIT_FAILURE, "%s: not a regular file", files[i]); + + snprintf(recs[i].path, sizeof recs[i].path, "%s", files[i]); + const char *base = strrchr(files[i], '/'); + base = base ? base + 1 : files[i]; + snprintf(recs[i].name, sizeof recs[i].name, "%s", base); + make_dos_name(recs[i].dos_name, files[i]); + recs[i].size = (unsigned)st.st_size; + recs[i].dos_time = to_dos_time(st.st_mtime); + } + + /* Load the SuperMaster (49152-byte built-in dictionary) */ + unsigned char *supermaster = malloc(49152); + if (!supermaster) + err(EXIT_FAILURE, "malloc"); + int sm_ret = uc2_get_supermaster(supermaster, 49152); + if (sm_ret < 0) + errx(EXIT_FAILURE, "Failed to load SuperMaster (%d)", sm_ret); + + FILE *out = fopen(opt.archive, "wb"); + if (!out) + err(EXIT_FAILURE, "%s", opt.archive); + + /* Placeholder FHEAD + XHEAD (29 bytes) */ + unsigned char header[29]; + memset(header, 0, sizeof header); + fwrite(header, 1, 29, out); + + /* Compress each file using SuperMaster dictionary */ + for (int i = 0; i < nfiles; i++) { + recs[i].offset = (unsigned)ftell(out); + + FILE *inf = fopen(recs[i].path, "rb"); + if (!inf) + err(EXIT_FAILURE, "%s", recs[i].path); + + unsigned csize = 0; + unsigned short csum = 0; + int ret = uc2_compress_ex(opt.level, supermaster, 49152, + fread_cb, inf, fwrite_cb, out, + recs[i].size, &csum, &csize); + fclose(inf); + if (ret < 0) + errx(EXIT_FAILURE, "%s: compression error %d", recs[i].path, ret); + + recs[i].csize = csize; + recs[i].csum = csum; + fprintf(stderr, " %s: %u -> %u\n", recs[i].name, recs[i].size, csize); + } + + /* Build raw central directory */ + unsigned cdir_cap = 22; /* EndOfCdir + XTAIL + aserial */ + for (int i = 0; i < nfiles; i++) + cdir_cap += 47 + 21 + (unsigned)strlen(recs[i].name) + 1; + + unsigned char *raw_cdir = malloc(cdir_cap); + if (!raw_cdir) + err(EXIT_FAILURE, "malloc"); + + unsigned char *p = raw_cdir; + for (int i = 0; i < nfiles; i++) { + unsigned namelen = (unsigned)strlen(recs[i].name); + /* OHEAD */ + *p++ = 2; /* FileEntry */ + /* OSMETA (22 bytes) */ + w32(p, 0); p += 4; /* parent = root */ + *p++ = 0x20; /* attrib = archive */ + w32(p, recs[i].dos_time); p += 4; + memcpy(p, recs[i].dos_name, 11); p += 11; + *p++ = 0; /* hidden */ + *p++ = 1; /* tag = has long name */ + /* FILEMETA (6 bytes) */ + w32(p, recs[i].size); p += 4; + w16(p, recs[i].csum); p += 2; + /* COMPRESS (10 bytes) */ + w32(p, recs[i].csize); p += 4; + w16(p, 1); p += 2; /* method = ultra */ + w32(p, 0); p += 4; /* masterPrefix = SuperMaster */ + /* LOCATION (8 bytes) */ + w32(p, 1); p += 4; /* volume */ + w32(p, recs[i].offset); p += 4; + /* EXTMETA: long name tag (21 + namelen + 1 bytes) */ + memcpy(p, "AIP:Win95 LongN", 16); p += 16; + w32(p, namelen + 1); p += 4; + *p++ = 0; /* next = no more tags */ + memcpy(p, recs[i].name, namelen + 1); p += namelen + 1; + } + + /* EndOfCdir */ + *p++ = 4; + /* XTAIL (17 bytes) */ + *p++ = 0; /* beta */ + *p++ = 0; /* lock */ + w32(p, 0); p += 4; /* serial */ + memset(p, ' ', 11); p += 11; /* label */ + /* aserial (4 bytes) */ + w32(p, 0); p += 4; + + unsigned cdir_size = (unsigned)(p - raw_cdir); + unsigned short cdir_csum = fletcher_csum(raw_cdir, cdir_size); + + /* COMPRESS record for cdir (placeholder) */ + unsigned cdir_offset = (unsigned)ftell(out); + unsigned char crec[10]; + memset(crec, 0, 10); + fwrite(crec, 1, 10, out); + + /* Compress cdir */ + struct mem_reader mr = {.data = raw_cdir, .pos = 0, .len = cdir_size}; + unsigned cdir_csize = 0; + unsigned short cdir_comp_csum = 0; + int ret = uc2_compress(opt.level, mem_read_cb, &mr, fwrite_cb, out, + cdir_size, &cdir_comp_csum, &cdir_csize); + free(raw_cdir); + if (ret < 0) + errx(EXIT_FAILURE, "cdir compression error %d", ret); + + unsigned total = (unsigned)ftell(out); + + /* Fix COMPRESS record for cdir */ + fseek(out, cdir_offset, SEEK_SET); + w32(crec + 0, cdir_csize); + w16(crec + 4, 1); + w32(crec + 6, 1); + fwrite(crec, 1, 10, out); + + /* Fix FHEAD + XHEAD */ + fseek(out, 0, SEEK_SET); + unsigned complen = total - 13; + w32(header + 0, 0x1A324355); /* "UC2\x1a" */ + w32(header + 4, complen); + w32(header + 8, complen + 0x01B2C3D4); + header[12] = 0; /* damageProtected */ + w32(header + 13, 1); /* cdir.volume */ + w32(header + 17, cdir_offset); /* cdir.offset */ + w16(header + 21, cdir_csum); /* fletch */ + header[23] = 0; /* busy */ + w16(header + 24, 300); /* versionMadeBy = 3.00 */ + w16(header + 26, 200); /* versionNeededToExtract */ + header[28] = 0; + fwrite(header, 1, 29, out); + + fclose(out); + free(supermaster); + free(recs); + printf("Created %s (%d file%s, %u bytes)\n", + opt.archive, nfiles, nfiles == 1 ? "" : "s", total); + return EXIT_SUCCESS; +} + int main(int argc, char *argv[]) { #ifdef __DJGPP__ @@ -478,7 +742,7 @@ int main(int argc, char *argv[]) goto usage; for (;;) { - int o = getopt(argc, argv, "xlatfd:C:cpDTh?"); + int o = getopt(argc, argv, "xlatfd:C:cpDTh?wL:"); if (o == -1) break; switch (o) { @@ -512,6 +776,14 @@ int main(int argc, char *argv[]) opt.no_file_meta = opt.no_dir_meta; opt.no_dir_meta = true; break; + case 'w': + opt.create = true; + break; + case 'L': + opt.level = atoi(optarg); + if (opt.level < 2 || opt.level > 5) + errx(EXIT_FAILURE, "Compression level must be 2..5"); + break; case 'T': opt.sep = '\t'; break; @@ -527,6 +799,7 @@ usage: "uc2 [-afpDT] [-d destination] archive.uc2 [files]...\n" "uc2 -l [-aT] archive.uc2 [files]...\n" "uc2 -t [-a] archive.uc2 [files]...\n" + "uc2 -w [-L level] archive.uc2 files...\n" ); if (!opt.help) printf("uc2 -h\n"); @@ -534,6 +807,8 @@ usage: printf( " -l List\n" " -t Test\n" + " -w Create archive\n" + " -L n Compression level: 2=Fast 3=Normal 4=Tight(default) 5=Ultra\n" " -a All versions of files\n" " -d path Destination to extract to\n" " -f Overwrite\n" @@ -550,6 +825,12 @@ usage: errx(EXIT_FAILURE, "Archive not given"); opt.archive = argv[optind++]; + if (opt.create) { + if (optind == argc) + errx(EXIT_FAILURE, "No files to add"); + return create_archive(argc - optind, argv + optind); + } + FILE *f = fopen(opt.archive, "rb"); if (!f) err(EXIT_FAILURE, "%s", opt.archive); diff --git a/docs/library.rst b/docs/library.rst index eca404a..39c5506 100644 --- a/docs/library.rst +++ b/docs/library.rst @@ -79,6 +79,21 @@ Compression :param compressed_size_out: Receives the compressed size. :returns: 0 on success, negative ``UC2_*`` error code on failure. +.. c:function:: int uc2_compress_ex(int level, const void *master, unsigned master_size, int (*read)(void *ctx, void *buf, unsigned len), void *read_ctx, int (*write)(void *ctx, const void *ptr, unsigned len), void *write_ctx, unsigned size, unsigned short *checksum_out, unsigned *compressed_size_out) + + Compress with a master-block dictionary prefix. The master data + pre-fills the LZ77 sliding window, allowing back-references into + the master for cross-file deduplication. Pass ``NULL`` / ``0`` for + no master (equivalent to :c:func:`uc2_compress`). + + The CLI uses the built-in SuperMaster (49 KB) by default. + +.. c:function:: int uc2_get_supermaster(void *buf, unsigned buf_size) + + Decompress the built-in SuperMaster into *buf* (must be at least + 49152 bytes). Returns ``49152`` on success, negative error code on + failure. + I/O Callbacks ------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst index affca11..71b730d 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -18,7 +18,9 @@ Basic Usage .. code-block:: sh + uc2 -w archive.uc2 file1 file2 # Create archive uc2 archive.uc2 # Extract all files uc2 -l archive.uc2 # List contents uc2 -t archive.uc2 # Test archive integrity uc2 -d /tmp/out archive.uc2 # Extract to directory + uc2 -w -L 5 big.uc2 data/* # Create with Ultra compression diff --git a/docs/usage.rst b/docs/usage.rst index a342395..90f4a20 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -7,6 +7,7 @@ Synopsis .. code-block:: none uc2 [options] archive.uc2 [patterns...] + uc2 -w [-L level] archive.uc2 files... Modes ----- @@ -24,6 +25,11 @@ Modes ``uc2 -p archive.uc2 filename`` Extract a file to stdout. +``uc2 -w archive.uc2 files...`` + Create a new archive from the given files. The original LZ77+Huffman + algorithm is used. Compression level defaults to 4 (Tight); use + ``-L`` to change it. + Options ------- @@ -37,6 +43,11 @@ Options - List archive contents * - ``-t`` - Test archive integrity + * - ``-w`` + - Create archive + * - ``-L n`` + - Compression level: 2 = Fast, 3 = Normal, 4 = Tight (default), + 5 = Ultra * - ``-a`` - Include all file versions (not just latest) * - ``-d path`` diff --git a/lib/include/uc2/libuc2.h b/lib/include/uc2/libuc2.h index ea9b47e..6d9a8a5 100644 --- a/lib/include/uc2/libuc2.h +++ b/lib/include/uc2/libuc2.h @@ -99,6 +99,27 @@ UC2_API int uc2_compress( unsigned *compressed_size_out ); +/* Compress with a master-block dictionary prefix. + The master data pre-fills the LZ77 sliding window, allowing + back-references into the master for cross-file deduplication. + Set master=NULL, master_size=0 for no master (same as uc2_compress). */ +UC2_API int uc2_compress_ex( + int level, + const void *master, unsigned master_size, + int (*read)(void *context, void *buf, unsigned len), + void *read_ctx, + int (*write)(void *context, const void *ptr, unsigned len), + void *write_ctx, + unsigned size, + unsigned short *checksum_out, + unsigned *compressed_size_out +); + +/* Decompress the built-in SuperMaster (49152 bytes). + buf must be at least 49152 bytes. + Returns 49152 on success, negative UC2_* error code on failure. */ +UC2_API int uc2_get_supermaster(void *buf, unsigned buf_size); + struct uc2_io { /* Read len bytes from the archive at offset pos into buf. Return number of bytes read, or less if eof. diff --git a/lib/src/compress.c b/lib/src/compress.c index 2619cb8..7edd9c0 100644 --- a/lib/src/compress.c +++ b/lib/src/compress.c @@ -813,8 +813,9 @@ static int count_write(void *ctx, const void *ptr, unsigned len) return cw->write(cw->ctx, ptr, len); } -int uc2_compress( +int uc2_compress_ex( int level, + const void *master, unsigned master_size, int (*read)(void *context, void *buf, unsigned len), void *read_ctx, int (*write)(void *context, const void *ptr, unsigned len), @@ -852,6 +853,19 @@ int uc2_compress( unsigned remaining = size; u16 load_pos = 0; + /* Pre-fill circular buffer with master data (LZ77 dictionary prefix) */ + if (master && master_size > 0) { + unsigned ms = master_size; + if (ms > UC2_BUF_SIZE - UC2_MIN_MATCH) + ms = UC2_BUF_SIZE - UC2_MIN_MATCH; + memcpy(c->data, master, ms); + for (unsigned i = 0; i + 2 < ms; i++) + hash_enter(c, (u16)i); + load_pos = (u16)ms; + c->pos = load_pos; + c->end = load_pos; + } + /* Pre-count EOB distance symbol frequency so the tree includes it */ c->bd_freq[NumByteSym + dist_to_sym(UC2_EOB_MARK)]++; c->l_freq[0]++; /* length = 3 for EOB marker */ @@ -945,3 +959,17 @@ int uc2_compress( free(ctx); return 0; } + +int uc2_compress( + int level, + int (*read)(void *context, void *buf, unsigned len), + void *read_ctx, + int (*write)(void *context, const void *ptr, unsigned len), + void *write_ctx, + unsigned size, + unsigned short *checksum_out, + unsigned *compressed_size_out) +{ + return uc2_compress_ex(level, NULL, 0, read, read_ctx, write, write_ctx, + size, checksum_out, compressed_size_out); +} diff --git a/lib/src/decompress.c b/lib/src/decompress.c index 31835af..08734bc 100644 --- a/lib/src/decompress.c +++ b/lib/src/decompress.c @@ -11,6 +11,7 @@ */ #include +#include #include #include #include @@ -1519,3 +1520,26 @@ const char *uc2_message(struct uc2_context *uc2, int ret) } return s; } + +/* Decompress the built-in SuperMaster (49152 bytes) into caller's buffer. + Returns 49152 on success, negative UC2_* error code on failure. */ +static void *sm_alloc(void *ctx, unsigned size) { (void)ctx; return malloc(size); } +static void sm_free(void *ctx, void *ptr) { (void)ctx; free(ptr); } + +int uc2_get_supermaster(void *buf, unsigned buf_size) +{ + if (buf_size < 49152) + return UC2_UserFault; + + struct uc2_io io = { .alloc = sm_alloc, .free = sm_free }; + struct uc2_context *uc2 = uc2_open(&io, NULL); + if (!uc2) + return UC2_UserFault; + + int ret = resolve_master(uc2, SuperMaster); + if (ret >= 0) + memcpy(buf, uc2->supermaster, 49152); + + uc2_close(uc2); + return ret < 0 ? ret : 49152; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index dada326..e369c9b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -20,3 +20,11 @@ target_link_libraries(test_roundtrip PRIVATE uc2) target_include_directories(test_roundtrip PRIVATE "${PROJECT_BINARY_DIR}/lib") target_compile_features(test_roundtrip PRIVATE c_std_99) add_test(NAME roundtrip COMMAND test_roundtrip) + +# CLI create/extract round-trip test +add_test(NAME cli_create + COMMAND ${CMAKE_COMMAND} + -DUC2_CLI=$ + -DTEST_DIR=${CMAKE_CURRENT_BINARY_DIR}/cli_test + -P ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_create.cmake +) diff --git a/tests/src/test_roundtrip.c b/tests/src/test_roundtrip.c index c88281a..9f8bb18 100644 --- a/tests/src/test_roundtrip.c +++ b/tests/src/test_roundtrip.c @@ -125,19 +125,20 @@ static void put_u32le(unsigned char *p, unsigned v) /* --- Build a minimal UC2 archive containing one file --- */ static int compress_data(const unsigned char *data, unsigned len, int level, + const void *master, unsigned master_size, struct membuf *out, unsigned short *csum_out) { struct mem_reader mr = { .data = data, .pos = 0, .len = len }; unsigned csize = 0; membuf_init(out); - int ret = uc2_compress(level, mem_read, &mr, membuf_write, out, - len, csum_out, &csize); + int ret = uc2_compress_ex(level, master, master_size, mem_read, &mr, + membuf_write, out, len, csum_out, &csize); return ret; } static int build_archive(const unsigned char *file_compressed, unsigned file_csize, unsigned file_orig_size, unsigned short file_csum, - int level, struct membuf *archive) + unsigned master_id, int level, struct membuf *archive) { /* * Archive layout: @@ -183,7 +184,7 @@ static int build_archive(const unsigned char *file_compressed, unsigned file_csi /* COMPRESS */ put_u32le(p, file_csize); p += 4; /* compressedLength */ put_u16le(p, 1); p += 2; /* method = ultra */ - put_u32le(p, 1); p += 4; /* masterPrefix = NoMaster */ + put_u32le(p, master_id); p += 4; /* masterPrefix */ /* LOCATION */ put_u32le(p, 1); p += 4; /* volume = 1 */ @@ -209,7 +210,7 @@ static int build_archive(const unsigned char *file_compressed, unsigned file_csi /* Compress the cdir */ struct membuf cdir_compressed; unsigned short cdir_compress_csum = 0; - int ret = compress_data(raw_cdir, raw_cdir_len, level, + int ret = compress_data(raw_cdir, raw_cdir_len, level, NULL, 0, &cdir_compressed, &cdir_compress_csum); if (ret < 0) { membuf_free(&cdir_compressed); @@ -258,15 +259,18 @@ static int build_archive(const unsigned char *file_compressed, unsigned file_csi return 0; } -static void test_roundtrip(const char *name, const unsigned char *input, - unsigned input_len, int level) +static void test_roundtrip_master(const char *name, const unsigned char *input, + unsigned input_len, int level, + const void *master, unsigned master_size, + unsigned master_id) { - printf(" %s (level %d, %u bytes): ", name, level, input_len); + printf(" %s (level %d, %u bytes, master=%u): ", name, level, input_len, master_id); /* Compress file data */ struct membuf file_compressed; unsigned short file_csum = 0; - int ret = compress_data(input, input_len, level, &file_compressed, &file_csum); + int ret = compress_data(input, input_len, level, master, master_size, + &file_compressed, &file_csum); if (ret < 0) { printf("FAIL (compress returned %d)\n", ret); failures++; @@ -278,7 +282,7 @@ static void test_roundtrip(const char *name, const unsigned char *input, /* Build archive */ struct membuf archive; ret = build_archive(file_compressed.data, file_compressed.len, - input_len, file_csum, level, &archive); + input_len, file_csum, master_id, level, &archive); membuf_free(&file_compressed); if (ret < 0) { printf("FAIL (build_archive returned %d)\n", ret); @@ -365,6 +369,12 @@ static void test_roundtrip(const char *name, const unsigned char *input, membuf_free(&output); } +static void test_roundtrip(const char *name, const unsigned char *input, + unsigned input_len, int level) +{ + test_roundtrip_master(name, input, input_len, level, NULL, 0, 1); +} + /* Test data generators */ static unsigned char *gen_zeros(unsigned len) @@ -467,6 +477,41 @@ int main(void) printf("\n"); } + /* SuperMaster round-trip tests (level 4 only for speed) */ + printf("SuperMaster:\n"); + { + unsigned char *sm = malloc(49152); + if (!sm) { fprintf(stderr, "malloc failed\n"); return EXIT_FAILURE; } + int sm_ret = uc2_get_supermaster(sm, 49152); + if (sm_ret < 0) { + fprintf(stderr, "uc2_get_supermaster failed: %d\n", sm_ret); + free(sm); + return EXIT_FAILURE; + } + + unsigned char empty = 0; + test_roundtrip_master("sm_empty", &empty, 0, 4, sm, 49152, 0); + + unsigned char one = 'A'; + test_roundtrip_master("sm_single", &one, 1, 4, sm, 49152, 0); + + unsigned char *z = gen_zeros(4096); + test_roundtrip_master("sm_zeros_4k", z, 4096, 4, sm, 49152, 0); + free(z); + + unsigned char *r = gen_random(1024, 42); + test_roundtrip_master("sm_random_1k", r, 1024, 4, sm, 49152, 0); + free(r); + + unsigned tlen; + unsigned char *t = gen_text(&tlen); + test_roundtrip_master("sm_text", t, tlen, 4, sm, 49152, 0); + free(t); + + free(sm); + } + printf("\n"); + if (failures) { fprintf(stderr, "%d test(s) FAILED\n", failures); return EXIT_FAILURE; diff --git a/tests/test_cli_create.cmake b/tests/test_cli_create.cmake new file mode 100644 index 0000000..c4968c7 --- /dev/null +++ b/tests/test_cli_create.cmake @@ -0,0 +1,63 @@ +# CLI create/extract round-trip test +# Creates test files, archives them, extracts, and verifies byte identity. + +file(REMOVE_RECURSE "${TEST_DIR}") +file(MAKE_DIRECTORY "${TEST_DIR}/input" "${TEST_DIR}/output") + +# Generate test files +file(WRITE "${TEST_DIR}/input/hello.txt" "Hello, UC2!\n") +file(WRITE "${TEST_DIR}/input/empty.dat" "") + +string(REPEAT "The quick brown fox jumps over the lazy dog.\n" 100 REPEATED) +file(WRITE "${TEST_DIR}/input/repeated.txt" "${REPEATED}") + +# Binary pattern (255 bytes, values 1..255 — avoid null which CMake can't handle) +foreach(i RANGE 1 255) + string(ASCII ${i} CH) + string(APPEND BINARY_DATA "${CH}") +endforeach() +file(WRITE "${TEST_DIR}/input/binary.dat" "${BINARY_DATA}") + +# Create archive +execute_process( + COMMAND "${UC2_CLI}" -w "${TEST_DIR}/test.uc2" + "${TEST_DIR}/input/hello.txt" + "${TEST_DIR}/input/empty.dat" + "${TEST_DIR}/input/repeated.txt" + "${TEST_DIR}/input/binary.dat" + RESULT_VARIABLE RC +) +if(NOT RC EQUAL 0) + message(FATAL_ERROR "uc2 -w failed: ${RC}") +endif() + +# List archive +execute_process( + COMMAND "${UC2_CLI}" -l "${TEST_DIR}/test.uc2" + OUTPUT_VARIABLE LISTING + RESULT_VARIABLE RC +) +if(NOT RC EQUAL 0) + message(FATAL_ERROR "uc2 -l failed: ${RC}") +endif() +message(STATUS "Archive listing:\n${LISTING}") + +# Extract +execute_process( + COMMAND "${UC2_CLI}" -d "${TEST_DIR}/output" "${TEST_DIR}/test.uc2" + RESULT_VARIABLE RC +) +if(NOT RC EQUAL 0) + message(FATAL_ERROR "uc2 extract failed: ${RC}") +endif() + +# Verify each file +foreach(F hello.txt empty.dat repeated.txt binary.dat) + file(READ "${TEST_DIR}/input/${F}" ORIGINAL) + file(READ "${TEST_DIR}/output/${F}" EXTRACTED) + if(NOT "${ORIGINAL}" STREQUAL "${EXTRACTED}") + message(FATAL_ERROR "${F}: content mismatch after round-trip") + endif() +endforeach() + +message(STATUS "cli_create: all files verified")