DVX_GUI/tools/dvxres.c

830 lines
21 KiB
C

// dvxres.c -- DVX resource tool
//
// Manages resource blocks appended to DXE3 files (.app, .wgt, .lib).
// Builds on both DOS (DJGPP) and Linux (GCC) with no dependencies
// beyond standard C.
//
// Commands:
// dvxres add <file> <name> <type> <data|@file>
// dvxres build <file> <manifest.res>
// dvxres list <file>
// dvxres get <file> <name> [outfile]
// dvxres strip <file>
//
// The resource block is appended after the DXE3 content:
// [resource data entries]
// [resource directory]
// [footer with magic + directory offset + count]
#include "../core/dvxRes.h"
#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// ============================================================
// Prototypes
// ============================================================
static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg);
static int cmdBuild(const char *dxePath, const char *manifestPath);
static int cmdGet(const char *dxePath, const char *name, const char *outPath);
static int cmdList(const char *dxePath);
static int cmdStrip(const char *dxePath);
static long dxeContentSize(const char *path);
static int parseType(const char *typeStr);
static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData);
static void usage(void);
// ============================================================
// dxeContentSize
// ============================================================
//
// Returns the size of the DXE3 content (before any appended resources).
// If the file has a resource footer, returns the start of the resource
// data. Otherwise returns the full file size.
static long dxeContentSize(const char *path) {
FILE *f = fopen(path, "rb");
if (!f) {
return -1;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
if (fileSize < (long)sizeof(DvxResFooterT)) {
fclose(f);
return fileSize;
}
// Check for resource footer
fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END);
DvxResFooterT footer;
if (fread(&footer, sizeof(footer), 1, f) == 1 && footer.magic == DVX_RES_MAGIC) {
// Find the earliest resource data offset
fseek(f, footer.dirOffset, SEEK_SET);
DvxResDirEntryT entry;
long earliest = footer.dirOffset;
for (uint32_t i = 0; i < footer.entryCount; i++) {
if (fread(&entry, sizeof(entry), 1, f) == 1) {
if ((long)entry.offset < earliest) {
earliest = (long)entry.offset;
}
}
}
fclose(f);
return earliest;
}
fclose(f);
return fileSize;
}
// ============================================================
// parseType
// ============================================================
static int parseType(const char *typeStr) {
if (strcmp(typeStr, "icon") == 0 || strcmp(typeStr, "image") == 0) {
return DVX_RES_ICON;
}
if (strcmp(typeStr, "text") == 0) {
return DVX_RES_TEXT;
}
if (strcmp(typeStr, "binary") == 0) {
return DVX_RES_BINARY;
}
return -1;
}
// ============================================================
// readExistingResources
// ============================================================
//
// Reads any existing resource block from the file. Returns arrays
// of directory entries and data blobs. Caller frees everything.
static int readExistingResources(const char *path, long dxeSize, DvxResDirEntryT **outEntries, uint32_t *outCount, uint8_t ***outData) {
*outEntries = NULL;
*outCount = 0;
*outData = NULL;
FILE *f = fopen(path, "rb");
if (!f) {
return -1;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
if (fileSize <= dxeSize) {
fclose(f);
return 0;
}
// Check for footer
fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END);
DvxResFooterT footer;
if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) {
fclose(f);
return 0;
}
// Read directory
fseek(f, footer.dirOffset, SEEK_SET);
DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));
if (!entries) {
fclose(f);
return -1;
}
if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
free(entries);
fclose(f);
return -1;
}
// Read each resource's data
uint8_t **data = (uint8_t **)malloc(footer.entryCount * sizeof(uint8_t *));
if (!data) {
free(entries);
fclose(f);
return -1;
}
for (uint32_t i = 0; i < footer.entryCount; i++) {
data[i] = (uint8_t *)malloc(entries[i].size);
if (!data[i]) {
for (uint32_t j = 0; j < i; j++) {
free(data[j]);
}
free(data);
free(entries);
fclose(f);
return -1;
}
fseek(f, entries[i].offset, SEEK_SET);
if (fread(data[i], 1, entries[i].size, f) != entries[i].size) {
fprintf(stderr, "Short read on entry %d\n", (int)i);
for (uint32_t j = 0; j <= i; j++) {
free(data[j]);
}
free(data);
free(entries);
fclose(f);
return -1;
}
}
fclose(f);
*outEntries = entries;
*outCount = footer.entryCount;
*outData = data;
return 0;
}
// ============================================================
// writeResources
// ============================================================
//
// Truncates the file to dxeSize and writes the resource block.
static int writeResources(const char *path, long dxeSize, DvxResDirEntryT *entries, uint32_t count, uint8_t **data) {
// Read the DXE content first
FILE *f = fopen(path, "rb");
if (!f) {
return -1;
}
uint8_t *dxeBuf = (uint8_t *)malloc(dxeSize);
if (!dxeBuf) {
fclose(f);
return -1;
}
if (fread(dxeBuf, 1, dxeSize, f) != (size_t)dxeSize) {
free(dxeBuf);
fclose(f);
return -1;
}
fclose(f);
// Rewrite file: DXE content + resources
f = fopen(path, "wb");
if (!f) {
free(dxeBuf);
return -1;
}
fwrite(dxeBuf, 1, dxeSize, f);
free(dxeBuf);
// Write resource data entries and update offsets
for (uint32_t i = 0; i < count; i++) {
entries[i].offset = (uint32_t)ftell(f);
fwrite(data[i], 1, entries[i].size, f);
}
// Write directory
uint32_t dirOffset = (uint32_t)ftell(f);
fwrite(entries, sizeof(DvxResDirEntryT), count, f);
// Write footer
DvxResFooterT footer;
footer.magic = DVX_RES_MAGIC;
footer.dirOffset = dirOffset;
footer.entryCount = count;
footer.reserved = 0;
fwrite(&footer, sizeof(footer), 1, f);
fclose(f);
return 0;
}
// ============================================================
// cmdAdd
// ============================================================
static int cmdAdd(const char *dxePath, const char *name, const char *typeStr, const char *dataArg) {
int type = parseType(typeStr);
if (type < 0) {
fprintf(stderr, "Unknown resource type: %s (use icon, text, or binary)\n", typeStr);
return 1;
}
if (strlen(name) >= DVX_RES_NAME_MAX) {
fprintf(stderr, "Resource name too long (max %d chars)\n", DVX_RES_NAME_MAX - 1);
return 1;
}
long dxeSize = dxeContentSize(dxePath);
if (dxeSize < 0) {
fprintf(stderr, "Cannot open: %s\n", dxePath);
return 1;
}
// Read existing resources
DvxResDirEntryT *entries = NULL;
uint32_t count = 0;
uint8_t **data = NULL;
readExistingResources(dxePath, dxeSize, &entries, &count, &data);
// Remove existing entry with same name (replace)
for (uint32_t i = 0; i < count; i++) {
if (strcmp(entries[i].name, name) == 0) {
free(data[i]);
for (uint32_t j = i; j < count - 1; j++) {
entries[j] = entries[j + 1];
data[j] = data[j + 1];
}
count--;
break;
}
}
// Prepare new resource data
uint8_t *newData = NULL;
uint32_t newSize = 0;
if (type == DVX_RES_TEXT) {
// Text data from command line argument
newSize = (uint32_t)strlen(dataArg) + 1;
newData = (uint8_t *)malloc(newSize);
if (!newData) {
fprintf(stderr, "Out of memory\n");
return 1;
}
memcpy(newData, dataArg, newSize);
} else {
// Binary/icon data from file
FILE *df = fopen(dataArg, "rb");
if (!df) {
fprintf(stderr, "Cannot open data file: %s\n", dataArg);
return 1;
}
fseek(df, 0, SEEK_END);
newSize = (uint32_t)ftell(df);
fseek(df, 0, SEEK_SET);
newData = (uint8_t *)malloc(newSize);
if (!newData) {
fclose(df);
fprintf(stderr, "Out of memory\n");
return 1;
}
if (fread(newData, 1, newSize, df) != newSize) {
fclose(df);
free(newData);
fprintf(stderr, "Failed to read: %s\n", dataArg);
return 1;
}
fclose(df);
}
// Append to arrays
entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT));
data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *));
memset(&entries[count], 0, sizeof(DvxResDirEntryT));
strncpy(entries[count].name, name, DVX_RES_NAME_MAX - 1);
entries[count].type = (uint32_t)type;
entries[count].size = newSize;
data[count] = newData;
count++;
// Write
int result = writeResources(dxePath, dxeSize, entries, count, data);
// Cleanup
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
if (result != 0) {
fprintf(stderr, "Failed to write resources to: %s\n", dxePath);
return 1;
}
printf("Added '%s' (%s, %u bytes) to %s\n", name, typeStr, (unsigned)newSize, dxePath);
return 0;
}
// ============================================================
// cmdBuild
// ============================================================
static int cmdBuild(const char *dxePath, const char *manifestPath) {
FILE *mf = fopen(manifestPath, "r");
if (!mf) {
fprintf(stderr, "Cannot open manifest: %s\n", manifestPath);
return 1;
}
long dxeSize = dxeContentSize(dxePath);
if (dxeSize < 0) {
fclose(mf);
fprintf(stderr, "Cannot open: %s\n", dxePath);
return 1;
}
// Strip any existing resources first -- start fresh
DvxResDirEntryT *entries = NULL;
uint32_t count = 0;
uint8_t **data = NULL;
char line[1024];
int lineNum = 0;
while (fgets(line, sizeof(line), mf)) {
lineNum++;
// Strip trailing newline/CR
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[--len] = '\0';
}
// Skip empty lines and comments
char *p = line;
while (*p && isspace((unsigned char)*p)) {
p++;
}
if (*p == '\0' || *p == '#') {
continue;
}
// Parse: name type data
// name and type are whitespace-delimited
// data is either a quoted string or a filename (rest of line)
char name[DVX_RES_NAME_MAX];
char typeStr[16];
char dataArg[512];
if (sscanf(p, "%31s %15s", name, typeStr) != 2) {
fprintf(stderr, "%s:%d: expected: name type data\n", manifestPath, lineNum);
goto fail;
}
// Advance past name and type to get data
char *dp = p;
// Skip name
while (*dp && !isspace((unsigned char)*dp)) {
dp++;
}
while (*dp && isspace((unsigned char)*dp)) {
dp++;
}
// Skip type
while (*dp && !isspace((unsigned char)*dp)) {
dp++;
}
while (*dp && isspace((unsigned char)*dp)) {
dp++;
}
if (*dp == '\0') {
fprintf(stderr, "%s:%d: missing data argument\n", manifestPath, lineNum);
goto fail;
}
// Handle quoted strings: strip quotes
if (*dp == '"') {
dp++;
char *end = strrchr(dp, '"');
if (end) {
*end = '\0';
}
}
strncpy(dataArg, dp, sizeof(dataArg) - 1);
dataArg[sizeof(dataArg) - 1] = '\0';
int type = parseType(typeStr);
if (type < 0) {
fprintf(stderr, "%s:%d: unknown type: %s\n", manifestPath, lineNum, typeStr);
goto fail;
}
// Prepare data
uint8_t *newData = NULL;
uint32_t newSize = 0;
if (type == DVX_RES_TEXT) {
newSize = (uint32_t)strlen(dataArg) + 1;
newData = (uint8_t *)malloc(newSize);
if (!newData) {
fprintf(stderr, "Out of memory\n");
goto fail;
}
memcpy(newData, dataArg, newSize);
} else {
FILE *df = fopen(dataArg, "rb");
if (!df) {
fprintf(stderr, "%s:%d: cannot open: %s\n", manifestPath, lineNum, dataArg);
goto fail;
}
fseek(df, 0, SEEK_END);
newSize = (uint32_t)ftell(df);
fseek(df, 0, SEEK_SET);
newData = (uint8_t *)malloc(newSize);
if (!newData) {
fclose(df);
fprintf(stderr, "Out of memory\n");
goto fail;
}
if (fread(newData, 1, newSize, df) != newSize) {
fclose(df);
free(newData);
fprintf(stderr, "%s:%d: failed to read: %s\n", manifestPath, lineNum, dataArg);
goto fail;
}
fclose(df);
}
// Append
entries = (DvxResDirEntryT *)realloc(entries, (count + 1) * sizeof(DvxResDirEntryT));
data = (uint8_t **)realloc(data, (count + 1) * sizeof(uint8_t *));
memset(&entries[count], 0, sizeof(DvxResDirEntryT));
snprintf(entries[count].name, DVX_RES_NAME_MAX, "%s", name);
entries[count].type = (uint32_t)type;
entries[count].size = newSize;
data[count] = newData;
count++;
printf(" %s (%s, %u bytes)\n", name, typeStr, (unsigned)newSize);
}
fclose(mf);
if (count == 0) {
printf("No resources in manifest.\n");
free(entries);
free(data);
return 0;
}
int result = writeResources(dxePath, dxeSize, entries, count, data);
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
if (result != 0) {
fprintf(stderr, "Failed to write resources to: %s\n", dxePath);
return 1;
}
printf("Built %u resource(s) into %s\n", (unsigned)count, dxePath);
return 0;
fail:
fclose(mf);
for (uint32_t i = 0; i < count; i++) {
free(data[i]);
}
free(data);
free(entries);
return 1;
}
// ============================================================
// cmdList
// ============================================================
static int cmdList(const char *dxePath) {
FILE *f = fopen(dxePath, "rb");
if (!f) {
fprintf(stderr, "Cannot open: %s\n", dxePath);
return 1;
}
fseek(f, -(long)sizeof(DvxResFooterT), SEEK_END);
DvxResFooterT footer;
if (fread(&footer, sizeof(footer), 1, f) != 1 || footer.magic != DVX_RES_MAGIC) {
fclose(f);
printf("No resources in %s\n", dxePath);
return 0;
}
fseek(f, footer.dirOffset, SEEK_SET);
printf("Resources in %s (%u entries):\n", dxePath, (unsigned)footer.entryCount);
printf(" %-31s %-8s %s\n", "Name", "Type", "Size");
printf(" %-31s %-8s %s\n", "-------------------------------", "--------", "--------");
for (uint32_t i = 0; i < footer.entryCount; i++) {
DvxResDirEntryT entry;
if (fread(&entry, sizeof(entry), 1, f) != 1) {
break;
}
const char *typeStr = "unknown";
if (entry.type == DVX_RES_ICON) {
typeStr = "icon";
} else if (entry.type == DVX_RES_TEXT) {
typeStr = "text";
} else if (entry.type == DVX_RES_BINARY) {
typeStr = "binary";
}
printf(" %-31s %-8s %u\n", entry.name, typeStr, (unsigned)entry.size);
}
fclose(f);
return 0;
}
// ============================================================
// cmdGet
// ============================================================
static int cmdGet(const char *dxePath, const char *name, const char *outPath) {
DvxResHandleT *h = dvxResOpen(dxePath);
if (!h) {
fprintf(stderr, "No resources in %s\n", dxePath);
return 1;
}
uint32_t size = 0;
void *buf = dvxResRead(h, name, &size);
dvxResClose(h);
if (!buf) {
fprintf(stderr, "Resource not found: %s\n", name);
return 1;
}
if (outPath) {
FILE *f = fopen(outPath, "wb");
if (!f) {
free(buf);
fprintf(stderr, "Cannot write: %s\n", outPath);
return 1;
}
fwrite(buf, 1, size, f);
fclose(f);
printf("Extracted '%s' (%u bytes) to %s\n", name, (unsigned)size, outPath);
} else {
// Write to stdout
fwrite(buf, 1, size, stdout);
}
free(buf);
return 0;
}
// ============================================================
// cmdStrip
// ============================================================
static int cmdStrip(const char *dxePath) {
long dxeSize = dxeContentSize(dxePath);
if (dxeSize < 0) {
fprintf(stderr, "Cannot open: %s\n", dxePath);
return 1;
}
// Read file size
FILE *f = fopen(dxePath, "rb");
if (!f) {
return 1;
}
fseek(f, 0, SEEK_END);
long fileSize = ftell(f);
fclose(f);
if (fileSize == dxeSize) {
printf("No resources to strip.\n");
return 0;
}
// Read DXE content
f = fopen(dxePath, "rb");
uint8_t *buf = (uint8_t *)malloc(dxeSize);
if (!buf) {
fclose(f);
fprintf(stderr, "Out of memory\n");
return 1;
}
if (fread(buf, 1, dxeSize, f) != (size_t)dxeSize) {
fprintf(stderr, "Short read on DXE file\n");
free(buf);
fclose(f);
return 1;
}
fclose(f);
// Rewrite truncated
f = fopen(dxePath, "wb");
if (!f) {
free(buf);
return 1;
}
fwrite(buf, 1, dxeSize, f);
fclose(f);
free(buf);
printf("Stripped %ld bytes of resources from %s\n", fileSize - dxeSize, dxePath);
return 0;
}
// ============================================================
// usage
// ============================================================
static void usage(void) {
fprintf(stderr, "DVX Resource Tool\n\n");
fprintf(stderr, "Usage:\n");
fprintf(stderr, " dvxres add <file> <name> <type> <data|@file>\n");
fprintf(stderr, " dvxres build <file> <manifest.res>\n");
fprintf(stderr, " dvxres list <file>\n");
fprintf(stderr, " dvxres get <file> <name> [outfile]\n");
fprintf(stderr, " dvxres strip <file>\n\n");
fprintf(stderr, "Types: icon (or image), text, binary\n\n");
fprintf(stderr, "For 'add' with type 'text', <data> is the string value.\n");
fprintf(stderr, "For 'add' with type 'icon' or 'binary', <data> is a file path.\n\n");
fprintf(stderr, "Manifest format (one per line):\n");
fprintf(stderr, " name type data\n");
fprintf(stderr, " # lines starting with # are comments\n");
}
// ============================================================
// main
// ============================================================
int main(int argc, char **argv) {
if (argc < 3) {
usage();
return 1;
}
const char *cmd = argv[1];
const char *file = argv[2];
if (strcmp(cmd, "add") == 0) {
if (argc < 6) {
fprintf(stderr, "Usage: dvxres add <file> <name> <type> <data>\n");
return 1;
}
return cmdAdd(file, argv[3], argv[4], argv[5]);
}
if (strcmp(cmd, "build") == 0) {
if (argc < 4) {
fprintf(stderr, "Usage: dvxres build <file> <manifest.res>\n");
return 1;
}
return cmdBuild(file, argv[3]);
}
if (strcmp(cmd, "list") == 0) {
return cmdList(file);
}
if (strcmp(cmd, "get") == 0) {
if (argc < 4) {
fprintf(stderr, "Usage: dvxres get <file> <name> [outfile]\n");
return 1;
}
return cmdGet(file, argv[3], argc >= 5 ? argv[4] : NULL);
}
if (strcmp(cmd, "strip") == 0) {
return cmdStrip(file);
}
fprintf(stderr, "Unknown command: %s\n", cmd);
usage();
return 1;
}