// 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 #include #include #include #include #include #include #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; }