DVX_GUI/src/installer/pkgRead.c
2026-09-29 17:03:51 -05:00

798 lines
23 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// pkgRead.c -- finding the DVX package and unpacking it (see pkgRead.h)
#include "pkgRead.h"
#include "fileops/fileOps.h"
#include "miniz.h"
#include "stb_ds_wrap.h"
#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include "dvxMem.h"
// Bytes read from a part at a time, and the work one step does
#define PKG_IN_BUF 16384
#define PKG_STEP_BYTES 32768
// Local header field offsets (after the signature)
#define LH_FLAGS 6
#define LH_METHOD 8
#define LH_TIME 10
#define LH_DATE 12
#define LH_CRC 14
#define LH_PACKED 18
#define LH_SIZE 22
#define LH_NAME_LEN 26
#define LH_EXTRA_LEN 28
#define ZIP_SIG_SIZE 4
// DOS date and time fields
#define DOS_YEAR_SHIFT 9
#define DOS_YEAR_MASK 0x7F
#define DOS_YEAR_BASE 1980
#define DOS_MONTH_SHIFT 5
#define DOS_MONTH_MASK 0x0F
#define DOS_DAY_MASK 0x1F
#define DOS_HOUR_SHIFT 11
#define DOS_MINUTE_SHIFT 5
#define DOS_MINUTE_MASK 0x3F
#define DOS_SECOND_MASK 0x1F
#define TM_YEAR_BASE 1900
#define PERCENT_FULL 100
typedef enum {
JobHeaderE, // next: a local header or the central directory
JobDataE, // unpacking a file
JobDoneE
} JobStateE;
struct PkgJobT {
PkgSourceT src;
char destDir[DVX_MAX_PATH];
PkgAskDiskFnT ask;
PkgKeepFnT keep;
void *user;
FILE *part;
int32_t disk;
uint32_t partLeft; // bytes of the part not read yet
uint32_t zipPos; // ZIP bytes read from all parts
uint8_t in[PKG_IN_BUF];
uint32_t inPos;
uint32_t inLen;
JobStateE state;
bool cancel;
bool failed;
char msg[PKG_MSG_MAX];
// the file being unpacked
char name[PKG_NAME_MAX];
char outPath[DVX_MAX_PATH];
FILE *out;
bool skipping; // kept: its data is read, not written
uint16_t method;
uint16_t dosDate;
uint16_t dosTime;
uint32_t crcWant;
uint32_t crcHave;
uint32_t packedLeft;
uint32_t size;
uint32_t sizeDone;
tinfl_decompressor inflator;
uint8_t dict[TINFL_LZ_DICT_SIZE];
uint32_t dictPos;
// totals
uint64_t bytesDone; // files finished
int32_t filesDone;
char **files;
char **written;
char **createdDirs;
};
// ============================================================
// Prototypes
// ============================================================
static void dirOfPath(const char *path, char *out, int32_t outSize);
static time_t dosTimeToTime(uint16_t date, uint16_t time);
static bool ensureDirs(PkgJobT *job, const char *relName, bool lastIsDir);
static void entryAbort(PkgJobT *job);
static PkgStepE entryData(PkgJobT *job);
static bool entryFinish(PkgJobT *job);
static PkgStepE entryHeader(PkgJobT *job);
static bool entryOpen(PkgJobT *job);
static PkgStepE fail(PkgJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static bool fill(PkgJobT *job);
static bool openPart(PkgJobT *job, int32_t disk);
static PkgDiskProblemE otherDisk(const PkgJobT *job, int32_t wanted);
static bool partPath(const PkgSourceT *src, int32_t disk, char *out, int32_t outSize);
void pkgFind(const char *exePath, PkgSourceT *src);
void pkgJobCancel(PkgJobT *job);
PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user);
char *const *pkgJobCreatedDirs(const PkgJobT *job);
char *const *pkgJobFiles(const PkgJobT *job);
void pkgJobFree(PkgJobT *job);
const char *pkgJobMessage(const PkgJobT *job);
void pkgJobProgress(const PkgJobT *job, PkgProgressT *out);
PkgStepE pkgJobStep(PkgJobT *job);
char *const *pkgJobWritten(const PkgJobT *job);
static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len);
static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len);
// The folder part of path ("." when it has none).
static void dirOfPath(const char *path, char *out, int32_t outSize) {
const char *end = platformPathDirEnd(path);
if (!end) {
snprintf(out, outSize, ".");
return;
}
snprintf(out, outSize, "%.*s", (int)(end - path), path);
}
static time_t dosTimeToTime(uint16_t date, uint16_t time) {
struct tm t;
memset(&t, 0, sizeof(t));
t.tm_year = ((date >> DOS_YEAR_SHIFT) & DOS_YEAR_MASK) + DOS_YEAR_BASE - TM_YEAR_BASE;
t.tm_mon = ((date >> DOS_MONTH_SHIFT) & DOS_MONTH_MASK) - 1;
t.tm_mday = date & DOS_DAY_MASK;
t.tm_hour = time >> DOS_HOUR_SHIFT;
t.tm_min = (time >> DOS_MINUTE_SHIFT) & DOS_MINUTE_MASK;
t.tm_sec = (time & DOS_SECOND_MASK) * 2;
t.tm_isdst = -1;
return mktime(&t);
}
// Make every folder of relName that does not exist yet (the last part
// too when lastIsDir), recording each one made.
static bool ensureDirs(PkgJobT *job, const char *relName, bool lastIsDir) {
int32_t len = (int32_t)strlen(relName);
for (int32_t i = 1; i <= len; i++) {
if (relName[i] != PKG_NAME_SEP && !(i == len && lastIsDir)) {
continue;
}
char rel[PKG_NAME_MAX];
char full[DVX_MAX_PATH];
struct stat st;
snprintf(rel, sizeof(rel), "%.*s", (int)i, relName);
if (!pkgLocalPath(job->destDir, rel, full, sizeof(full))) {
fail(job, "The path of %s is too long.", rel);
return false;
}
if (stat(full, &st) == 0) {
continue;
}
if (platformMkdirRecursive(full) != 0) {
fail(job, "Setup could not create the folder %s.", full);
return false;
}
arrput(job->createdDirs, strdup(full));
}
return true;
}
// Close and remove a half-written file.
static void entryAbort(PkgJobT *job) {
if (!job->out) {
return;
}
fclose(job->out);
job->out = NULL;
remove(job->outPath);
if (arrlen(job->written) > 0) {
free(arrpop(job->written));
}
}
// Unpack (or skip) up to PKG_STEP_BYTES of the current file.
static PkgStepE entryData(PkgJobT *job) {
uint32_t work = 0;
while (work < PKG_STEP_BYTES) {
if (job->packedLeft > 0 && !fill(job)) {
return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
}
uint32_t avail = job->inLen - job->inPos;
if (avail > job->packedLeft) {
avail = job->packedLeft;
}
bool done;
if (job->skipping || job->method == PKG_ZIP_STORED) {
if (!job->skipping && !writeOut(job, job->in + job->inPos, avail)) {
return PkgStepErrorE;
}
job->inPos += avail;
job->packedLeft -= avail;
work += avail;
done = job->packedLeft == 0;
} else {
size_t inBytes = avail;
size_t outBytes = TINFL_LZ_DICT_SIZE - job->dictPos;
int flags = job->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0;
tinfl_status status = tinfl_decompress(&job->inflator, job->in + job->inPos, &inBytes, job->dict, job->dict + job->dictPos, &outBytes, (mz_uint32)flags);
job->inPos += (uint32_t)inBytes;
job->packedLeft -= (uint32_t)inBytes;
work += (uint32_t)(inBytes + outBytes);
if (outBytes > 0 && !writeOut(job, job->dict + job->dictPos, (uint32_t)outBytes)) {
return PkgStepErrorE;
}
job->dictPos = (job->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1);
if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && job->packedLeft == 0)) {
entryAbort(job);
return fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
}
done = status == TINFL_STATUS_DONE;
}
if (done) {
return entryFinish(job) ? PkgStepMoreE : PkgStepErrorE;
}
}
return PkgStepMoreE;
}
// The file's data has all been read: check it, close it, date it.
static bool entryFinish(PkgJobT *job) {
if (!job->skipping) {
if (job->packedLeft != 0 || job->sizeDone != job->size || job->crcHave != job->crcWant) {
entryAbort(job);
fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
return false;
}
FILE *f = job->out;
job->out = NULL;
if (fclose(f) != 0) {
remove(job->outPath);
fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
return false;
}
fileOpsSetTime(job->outPath, dosTimeToTime(job->dosDate, job->dosTime));
}
job->bytesDone += job->size;
job->filesDone++;
job->state = JobHeaderE;
return true;
}
// The next local header (or the central directory: the end).
static PkgStepE entryHeader(PkgJobT *job) {
uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
if (!readBytes(job, hdr, ZIP_SIG_SIZE)) {
return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
}
uint32_t sig = pkgGet32(hdr);
if (sig == PKG_ZIP_CENTRAL_SIG) {
if (job->filesDone != (int32_t)job->src.info.fileCount) {
return fail(job, "The DVX package is incomplete: %d of %d files.", (int)job->filesDone, (int)job->src.info.fileCount);
}
job->state = JobDoneE;
return PkgStepDoneE;
}
if (sig != PKG_ZIP_LOCAL_SIG) {
return fail(job, "The DVX package is damaged (disk %d).", (int)job->disk);
}
if (!readBytes(job, hdr + ZIP_SIG_SIZE, PKG_ZIP_LOCAL_SIZE - ZIP_SIG_SIZE)) {
return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
}
uint16_t flags = pkgGet16(hdr + LH_FLAGS);
uint16_t nameLen = pkgGet16(hdr + LH_NAME_LEN);
uint16_t extraLen = pkgGet16(hdr + LH_EXTRA_LEN);
job->method = pkgGet16(hdr + LH_METHOD);
job->dosTime = pkgGet16(hdr + LH_TIME);
job->dosDate = pkgGet16(hdr + LH_DATE);
job->crcWant = pkgGet32(hdr + LH_CRC);
job->packedLeft = pkgGet32(hdr + LH_PACKED);
job->size = pkgGet32(hdr + LH_SIZE);
if ((flags & (PKG_ZIP_FLAG_DESC | PKG_ZIP_FLAG_CRYPT)) || (job->method != PKG_ZIP_STORED && job->method != PKG_ZIP_DEFLATED) || nameLen == 0 || nameLen >= PKG_NAME_MAX) {
return fail(job, "The DVX package uses a form Setup cannot read (disk %d).", (int)job->disk);
}
if (!readBytes(job, (uint8_t *)job->name, nameLen)) {
return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
}
job->name[nameLen] = '\0';
for (uint16_t left = extraLen; left > 0; ) {
uint8_t skip[PKG_ZIP_LOCAL_SIZE];
uint16_t n = left < sizeof(skip) ? left : (uint16_t)sizeof(skip);
if (!readBytes(job, skip, n)) {
return job->cancel ? PkgStepCancelledE : PkgStepErrorE;
}
left -= n;
}
bool isDir = job->name[nameLen - 1] == PKG_NAME_SEP;
if (isDir) {
job->name[nameLen - 1] = '\0';
}
if (!pkgNameIsValid(job->name)) {
return fail(job, "The DVX package holds a name Setup does not accept: %s", job->name);
}
if (isDir) {
if (!ensureDirs(job, job->name, true)) {
return PkgStepErrorE;
}
char *listed = (char *)malloc(nameLen + 1);
if (listed) {
snprintf(listed, nameLen + 1, "%s%c", job->name, PKG_NAME_SEP);
arrput(job->files, listed);
}
return PkgStepMoreE;
}
return entryOpen(job) ? PkgStepMoreE : PkgStepErrorE;
}
// Set up the file of the header just read: kept, or opened for writing.
static bool entryOpen(PkgJobT *job) {
struct stat st;
if (!ensureDirs(job, job->name, false)) {
return false;
}
if (!pkgLocalPath(job->destDir, job->name, job->outPath, sizeof(job->outPath))) {
fail(job, "The path of %s is too long.", job->name);
return false;
}
arrput(job->files, strdup(job->name));
job->skipping = stat(job->outPath, &st) == 0 && job->keep && job->keep(job->user, job->name);
job->sizeDone = 0;
job->crcHave = (uint32_t)MZ_CRC32_INIT;
job->dictPos = 0;
job->state = JobDataE;
tinfl_init(&job->inflator);
if (job->skipping) {
return true;
}
job->out = fopen(job->outPath, "wb");
// A read-only copy (an older install copied from a CD) is replaced.
if (!job->out && fileOpsSetAttr(job->outPath, 0)) {
job->out = fopen(job->outPath, "wb");
}
if (!job->out) {
fail(job, "Setup could not write %s. The disk may be full or write-protected.", job->outPath);
return false;
}
arrput(job->written, strdup(job->outPath));
return true;
}
static PkgStepE fail(PkgJobT *job, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
vsnprintf(job->msg, sizeof(job->msg), fmt, ap);
va_end(ap);
job->failed = true;
return PkgStepErrorE;
}
// Make sure the input buffer has a byte, opening the next part when this
// one is used up. False on an error (message set) or a cancel.
static bool fill(PkgJobT *job) {
if (job->inPos < job->inLen) {
return true;
}
while (job->partLeft == 0) {
if (job->disk >= job->src.info.diskCount) {
fail(job, "The DVX package ends too early (disk %d).", (int)job->disk);
return false;
}
if (!openPart(job, job->disk + 1)) {
return false;
}
}
uint32_t n = job->partLeft < PKG_IN_BUF ? job->partLeft : PKG_IN_BUF;
if (fread(job->in, 1, n, job->part) != n) {
fail(job, "Setup could not read the DVX package on disk %d.", (int)job->disk);
return false;
}
job->inPos = 0;
job->inLen = n;
job->partLeft -= n;
job->zipPos += n;
return true;
}
// Open disk's part (asking for the disk until it is the right one) and
// position it at its first byte. False on a cancel or an error.
static bool openPart(PkgJobT *job, int32_t disk) {
char path[DVX_MAX_PATH];
if (job->part) {
fclose(job->part);
job->part = NULL;
}
if (job->src.found == PkgFoundAppendedE) {
snprintf(path, sizeof(path), "%s", job->src.path);
} else if (!partPath(&job->src, disk, path, sizeof(path))) {
fail(job, "The folder %s is too long.", job->src.partDir);
return false;
}
for (;;) {
PkgHeaderT h;
uint32_t start = 0;
PkgDiskProblemE problem = PkgDiskMissingE;
FILE *f = fopen(path, "rb");
if (f) {
if (!pkgHeaderRead(f, &h, &start)) {
problem = PkgDiskDamagedE;
} else if (h.setId != job->src.info.setId) {
problem = PkgDiskOtherSetE;
} else if (h.disk != disk || h.partOffset != job->zipPos) {
problem = PkgDiskWrongE;
} else if (fseek(f, (long)start, SEEK_SET) != 0) {
problem = PkgDiskDamagedE;
} else {
job->part = f;
job->disk = disk;
job->partLeft = h.partLength;
return true;
}
fclose(f);
} else {
problem = otherDisk(job, disk);
}
if (job->src.found == PkgFoundAppendedE || !job->ask) {
fail(job, "Setup could not read the DVX package in %s.", path);
return false;
}
if (!job->ask(job->user, disk, problem)) {
job->cancel = true;
return false;
}
}
}
// The wanted part is not there: is another disk's part (of this set or
// another one) in the drive instead? The disk before the wanted one is
// the normal case (not swapped yet): just missing.
static PkgDiskProblemE otherDisk(const PkgJobT *job, int32_t wanted) {
char **names = dvxReadDir(job->src.partDir);
PkgDiskProblemE problem = PkgDiskMissingE;
for (int32_t i = 0; i < (int32_t)arrlen(names) && problem == PkgDiskMissingE; i++) {
char upper[PKG_PART_NAME_MAX];
char path[DVX_MAX_PATH];
int partNo = 0; // sscanf's %d wants an int
char end = '\0';
uint32_t start = 0;
PkgHeaderT h;
snprintf(upper, sizeof(upper), "%s", names[i]);
for (char *p = upper; *p; p++) {
*p = (char)toupper((unsigned char)*p);
}
if (strlen(names[i]) >= sizeof(upper) || sscanf(upper, PKG_PART_SCAN "%c", &partNo, &end) != 1 || !fileOpsPathJoin(path, sizeof(path), job->src.partDir, names[i])) {
continue;
}
FILE *f = fopen(path, "rb");
if (!f) {
continue;
}
if (!pkgHeaderRead(f, &h, &start)) {
problem = PkgDiskDamagedE;
} else if (h.setId != job->src.info.setId) {
problem = PkgDiskOtherSetE;
} else if (h.disk != wanted - 1) {
problem = PkgDiskWrongE;
}
fclose(f);
}
dvxReadDirFree(names);
return problem;
}
static bool partPath(const PkgSourceT *src, int32_t disk, char *out, int32_t outSize) {
char name[PKG_PART_NAME_MAX];
return pkgPartName(disk, name, sizeof(name)) && fileOpsPathJoin(out, outSize, src->partDir, name);
}
void pkgFind(const char *exePath, PkgSourceT *src) {
PkgHeaderT h;
uint32_t start = 0;
memset(&h, 0, sizeof(h));
memset(src, 0, sizeof(*src));
src->found = PkgFoundNoneE;
dirOfPath(exePath, src->partDir, sizeof(src->partDir));
FILE *f = fopen(exePath, "rb");
if (f) {
bool appended = pkgHeaderRead(f, &h, &start) && h.diskCount == 1 && h.partOffset == 0 && h.partLength == h.zipSize;
fclose(f);
if (appended) {
src->found = PkgFoundAppendedE;
src->info = h;
snprintf(src->path, sizeof(src->path), "%s", exePath);
return;
}
}
if (!partPath(src, 1, src->path, sizeof(src->path))) {
return;
}
f = fopen(src->path, "rb");
if (!f) {
return;
}
src->found = pkgHeaderRead(f, &h, &start) && h.disk == 1 && h.partOffset == 0 ? PkgFoundPartsE : PkgFoundDamagedE;
src->info = h;
fclose(f);
}
void pkgJobCancel(PkgJobT *job) {
job->cancel = true;
}
PkgJobT *pkgJobCreate(const PkgSourceT *src, const char *destDir, PkgAskDiskFnT ask, PkgKeepFnT keep, void *user) {
PkgJobT *job = (PkgJobT *)calloc(1, sizeof(PkgJobT));
if (!job) {
return NULL;
}
job->src = *src;
job->ask = ask;
job->keep = keep;
job->user = user;
job->state = JobHeaderE;
snprintf(job->destDir, sizeof(job->destDir), "%s", destDir);
// Disk 1 is the one pkgFind looked at; asking again is only for the
// disks after it.
if (!openPart(job, 1)) {
job->state = JobDoneE;
}
return job;
}
char *const *pkgJobCreatedDirs(const PkgJobT *job) {
return job->createdDirs;
}
char *const *pkgJobFiles(const PkgJobT *job) {
return job->files;
}
void pkgJobFree(PkgJobT *job) {
if (!job) {
return;
}
entryAbort(job);
if (job->part) {
fclose(job->part);
}
for (int32_t i = 0; i < (int32_t)arrlen(job->files); i++) {
free(job->files[i]);
}
for (int32_t i = 0; i < (int32_t)arrlen(job->written); i++) {
free(job->written[i]);
}
for (int32_t i = 0; i < (int32_t)arrlen(job->createdDirs); i++) {
free(job->createdDirs[i]);
}
arrfree(job->files);
arrfree(job->written);
arrfree(job->createdDirs);
free(job);
}
const char *pkgJobMessage(const PkgJobT *job) {
return job->msg;
}
void pkgJobProgress(const PkgJobT *job, PkgProgressT *out) {
uint64_t done = job->bytesDone + (job->state == JobDataE ? job->sizeDone : 0);
memset(out, 0, sizeof(*out));
out->bytesTotal = job->src.info.unpackedBytes;
out->bytesDone = done > out->bytesTotal ? out->bytesTotal : done;
out->filesDone = job->filesDone;
out->fileCount = (int32_t)job->src.info.fileCount;
out->percent = out->bytesTotal ? (int32_t)(out->bytesDone * PERCENT_FULL / out->bytesTotal) : PERCENT_FULL;
out->disk = job->disk;
out->current = job->state == JobDataE ? job->name : "";
}
PkgStepE pkgJobStep(PkgJobT *job) {
if (job->failed) {
return PkgStepErrorE;
}
if (job->cancel) {
entryAbort(job);
return PkgStepCancelledE;
}
switch (job->state) {
case JobHeaderE:
return entryHeader(job);
case JobDataE:
return entryData(job);
default:
return PkgStepDoneE;
}
}
char *const *pkgJobWritten(const PkgJobT *job) {
return job->written;
}
// Exactly len bytes of the stream, across parts.
static bool readBytes(PkgJobT *job, uint8_t *buf, uint32_t len) {
while (len > 0) {
if (!fill(job)) {
return false;
}
uint32_t n = job->inLen - job->inPos;
if (n > len) {
n = len;
}
memcpy(buf, job->in + job->inPos, n);
job->inPos += n;
buf += n;
len -= n;
}
return true;
}
static bool writeOut(PkgJobT *job, const uint8_t *data, uint32_t len) {
if (job->sizeDone + len > job->size) {
entryAbort(job);
fail(job, "The file %s in the DVX package is damaged (disk %d).", job->name, (int)job->disk);
return false;
}
if (fwrite(data, 1, len, job->out) != len) {
entryAbort(job);
fail(job, "Setup could not write %s. The disk may be full.", job->outPath);
return false;
}
job->crcHave = (uint32_t)mz_crc32(job->crcHave, data, len);
job->sizeDone += len;
return true;
}