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.
This commit is contained in:
Eremey Valetov
2026-03-12 02:04:13 -04:00
parent 7691dcc4fa
commit a30c8cf694
10 changed files with 511 additions and 13 deletions
+283 -2
View File
@@ -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);
+15
View File
@@ -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
-------------
+2
View File
@@ -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
+11
View File
@@ -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``
+21
View File
@@ -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.
+29 -1
View File
@@ -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);
}
+24
View File
@@ -11,6 +11,7 @@
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <assert.h>
@@ -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;
}
+8
View File
@@ -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=$<TARGET_FILE:uc2-cli>
-DTEST_DIR=${CMAKE_CURRENT_BINARY_DIR}/cli_test
-P ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_create.cmake
)
+55 -10
View File
@@ -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;
+63
View File
@@ -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")