// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // xdftool.c - read and write Human68k FAT12 floppy images (.XDF). // // Why this exists: mtools cannot handle these images. Human68k 2HD floppies // use 1024-byte logical sectors, and mformat mis-sizes the volume while // mcopy fails with rc=1 regardless of configuration. imgtool has no // Human68k module. So JoeyLib's X68000 runs need their own reader and // writer to get a program onto a disk and its log back off again. // // The filesystem itself is ordinary FAT12; only the sector size is unusual, // and every offset in the BPB is already expressed in sectors, so nothing // needs special-casing beyond reading bytesPerSector rather than assuming // 512. Human68k's extended names are kept: a name's stem may run to 18 // characters, the first 8 in the classic name field and the rest in the // directory entry's bytes 12-21 (MS-DOS's reserved area). Human68k keeps a // name's case as written, so names are matched without regard to case. // // This does NOT create bootable images. Mint one once by running Human68k's // own FORMAT.X/SYS.X inside an emulator (they drive the FDD through IOCS), // keep it as a template, and copy it per run. // // xdftool list IMAGE // xdftool add IMAGE HOSTFILE [NAME] NAME may be SUB/NAME or a // deeper path: folders are // made as needed // xdftool delete IMAGE NAME // xdftool deltree IMAGE NAME a root entry and all under it // xdftool empty IMAGE every root entry but the // volume label, and all under // xdftool extract IMAGE NAME HOSTFILE // xdftool free IMAGE #include #include #include #include #include #include "toolFile.h" #define TOOL_NAME "xdftool" #define USAGE_EXIT 2 #define DIR_ENTRY_SIZE 32u #define ATTR_VOLUME 0x08u #define ATTR_DIR 0x10u #define ATTR_ARCHIVE 0x20u #define FREE_MARKER 0x00u #define DELETED_MARKER 0xE5u // A FAT12 entry of this or more ends a chain; a chain's last cluster gets // EOC. #define EOC_MIN 0xFF8u #define EOC 0xFFFu #define FAT12_MASK 0x0FFFu #define FAT12_HIGH_SHIFT 4u #define FIRST_CLUSTER 2u // The BPB: bytes per sector at 11, sectors per cluster at 13, reserved // sectors at 14, FAT copies at 16, root entries at 17, total sectors at 19, // sectors per FAT at 22. #define BPB_END 30u #define BPB_BPS 11u #define BPB_SPC 13u #define BPB_RESERVED 14u #define BPB_NFATS 16u #define BPB_ROOT_ENTRIES 17u #define BPB_TOTAL_SECTORS 19u #define BPB_SPF 22u // A directory entry: the 8.3 name at 0, attributes at 11, Human68k's // extended stem at 12 (10 bytes), time at 22, date at 24, first cluster at // 26, size at 28. #define ENTRY_NAME_BYTES 11u #define ENTRY_STEM_BYTES 8u #define ENTRY_EXT_BYTES 3u #define ENTRY_ATTR 11u #define ENTRY_EXTENDED 12u #define ENTRY_EXTENDED_LEN 10u #define ENTRY_TIME 22u #define ENTRY_DATE 24u #define ENTRY_CLUSTER 26u #define ENTRY_SIZE 28u #define STEM_MAX (ENTRY_STEM_BYTES + ENTRY_EXTENDED_LEN) // Every entry written is stamped 2026-01-01 12:00:00 (FAT time and date). #define ENTRY_TIME_VALUE (12u << 11) #define ENTRY_DATE_VALUE (((2026u - 1980u) << 9) | (1u << 5) | 1u) #define ASCII_END 0x80u #define NAME_TEXT_MAX 64u #define LIST_NAME_WIDTH 14u // A subtree is walked at most this deep (a damaged image's loop stops). #define TREE_DEPTH_MAX 64u #define NOT_FOUND SIZE_MAX typedef struct XdfT { const char *path; uint8_t *data; size_t len; uint32_t bps; uint32_t spc; uint32_t reserved; uint32_t nfats; uint32_t rootEntries; uint32_t totalSectors; uint32_t spf; size_t fatStart; size_t rootStart; size_t dataStart; size_t clusterBytes; uint32_t maxCluster; } XdfT; // A name as a directory entry holds it: the 8.3 field and the extended // stem, NUL-padded (all NUL for a plain 8.3 name). typedef struct NameT { uint8_t main[ENTRY_NAME_BYTES]; uint8_t extended[ENTRY_EXTENDED_LEN]; } NameT; // A walk over a directory's entries: the fixed root table (cluster 0) or a // subdirectory's cluster chain. typedef struct DirIterT { uint32_t cluster; uint32_t index; uint32_t guard; bool root; } DirIterT; static void addFile(XdfT *x, const char *name, const uint8_t *payload, size_t len); static void addFileAtPath(XdfT *x, const char *path, const uint8_t *payload, size_t len); static void addFileInSubdir(XdfT *x, const char *dirName, const char *name, const uint8_t *payload, size_t len); static uint32_t allocWritePayload(XdfT *x, const uint8_t *payload, size_t len); static size_t clusterOffset(const XdfT *x, uint32_t cluster); static bool deleteRoot(XdfT *x, const char *name); static bool deleteTree(XdfT *x, const char *name); static bool dirCluster(XdfT *x, const char *path, size_t pathLen, bool create, uint32_t *cluster); static void dirEntry(uint8_t *out, const uint8_t *name11, uint32_t attr, uint32_t cluster, uint32_t size, const uint8_t *extended); static void dirIterStart(DirIterT *it, uint32_t cluster); static size_t dirNext(const XdfT *x, DirIterT *it); static uint32_t emptyRoot(XdfT *x); static bool entryMatches(const uint8_t *e, const NameT *name); static void entryName(const uint8_t *e, char *out, size_t outSize, size_t *chars); static uint32_t fatGet(const XdfT *x, uint32_t cluster); static void fatSet(XdfT *x, uint32_t cluster, uint32_t value); static size_t findEntry(const XdfT *x, uint32_t cluster, const char *name, size_t nameLen, bool filesOnly); static void freeChain(XdfT *x, uint32_t cluster); static uint32_t freeCount(const XdfT *x); static uint32_t freeNth(const XdfT *x, uint32_t n); static void freeSubtree(XdfT *x, uint32_t cluster, uint32_t depth); static void load(const char *path, XdfT *x); static void nameEncode(const char *name, size_t len, NameT *out); static void putEntryAny(XdfT *x, uint32_t cluster, const uint8_t *entry); static void putEntryInDir(XdfT *x, uint32_t firstCluster, const uint8_t *entry); static uint16_t rd16(const uint8_t *p); static uint32_t rd32(const uint8_t *p); static uint8_t *readFile(XdfT *x, const char *name, size_t *len); static void save(const XdfT *x); static void usage(void); static void wr16(uint8_t *p, uint32_t v); static void wr32(uint8_t *p, uint32_t v); static const uint8_t kDotName[ENTRY_NAME_BYTES] = { '.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }; static const uint8_t kDotDotName[ENTRY_NAME_BYTES] = { '.', '.', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' }; // A file into the root (one of the same name there first deleted). static void addFile(XdfT *x, const char *name, const uint8_t *payload, size_t len) { NameT encoded; uint32_t head; uint8_t entry[DIR_ENTRY_SIZE]; DirIterT it; size_t off; deleteRoot(x, name); nameEncode(name, strlen(name), &encoded); head = allocWritePayload(x, payload, len); dirIterStart(&it, 0u); while ((off = dirNext(x, &it)) != NOT_FOUND) { if (x->data[off] == FREE_MARKER || x->data[off] == DELETED_MARKER) { dirEntry(entry, encoded.main, ATTR_ARCHIVE, head, (uint32_t)len, encoded.extended); memcpy(x->data + off, entry, DIR_ENTRY_SIZE); return; } } toolFail("%s: root directory full (%u entries)", x->path, x->rootEntries); } // A file at a path ("DATA/LEVELS/LEVEL01.DAT"): its folders made as needed, // then the file written into the last. static void addFileAtPath(XdfT *x, const char *path, const uint8_t *payload, size_t len) { const char *slash; uint32_t cluster; uint32_t head; uint8_t entry[DIR_ENTRY_SIZE]; NameT encoded; slash = strrchr(path, '/'); cluster = 0u; if (slash != path) { dirCluster(x, path, (size_t)(slash - path), true, &cluster); } nameEncode(slash + 1, strlen(slash + 1), &encoded); head = allocWritePayload(x, payload, len); dirEntry(entry, encoded.main, ATTR_ARCHIVE, head, (uint32_t)len, encoded.extended); putEntryAny(x, cluster, entry); } // A file into a folder of the root (SAVES/RN.CFG): into the folder when it // is there, else into a new one-cluster folder with its . and .. entries. static void addFileInSubdir(XdfT *x, const char *dirName, const char *name, const uint8_t *payload, size_t len) { NameT encoded; uint8_t entry[DIR_ENTRY_SIZE]; uint8_t stem[ENTRY_NAME_BYTES]; uint32_t dirClusterNumber; uint32_t head; size_t dirLen; size_t found; size_t off; size_t i; DirIterT it; dirLen = strlen(dirName); if (dirLen > ENTRY_STEM_BYTES) { toolFail("subdir '%s' does not fit 8.3", dirName); } nameEncode(name, strlen(name), &encoded); found = findEntry(x, 0u, dirName, dirLen, false); if (found != NOT_FOUND && (x->data[found + ENTRY_ATTR] & ATTR_DIR) != 0u) { head = allocWritePayload(x, payload, len); dirEntry(entry, encoded.main, ATTR_ARCHIVE, head, (uint32_t)len, encoded.extended); putEntryInDir(x, rd16(x->data + found + ENTRY_CLUSTER), entry); return; } // A fresh folder: its cluster reserved first so the payload cannot // take it, then '.', '..', and the file. if (freeCount(x) == 0u) { toolFail("%s: no free cluster for subdirectory", x->path); } dirClusterNumber = freeNth(x, 0u); fatSet(x, dirClusterNumber, EOC); head = allocWritePayload(x, payload, len); off = clusterOffset(x, dirClusterNumber); memset(x->data + off, 0, x->clusterBytes); dirEntry(x->data + off, kDotName, ATTR_DIR, dirClusterNumber, 0u, NULL); dirEntry(x->data + off + DIR_ENTRY_SIZE, kDotDotName, ATTR_DIR, 0u, 0u, NULL); dirEntry(x->data + off + 2u * DIR_ENTRY_SIZE, encoded.main, ATTR_ARCHIVE, head, (uint32_t)len, encoded.extended); memset(stem, ' ', sizeof(stem)); for (i = 0; i < dirLen; i++) { if ((unsigned char)dirName[i] >= ASCII_END) { toolFail("'%s' is not ASCII", dirName); } stem[i] = (uint8_t)toupper((unsigned char)dirName[i]); } dirIterStart(&it, 0u); while ((off = dirNext(x, &it)) != NOT_FOUND) { if (x->data[off] == FREE_MARKER || x->data[off] == DELETED_MARKER) { dirEntry(x->data + off, stem, ATTR_DIR, dirClusterNumber, 0u, NULL); return; } } toolFail("%s: root directory full", x->path); } // A FAT chain for payload, written (the last cluster zero-padded so stale // bytes never leak); its first cluster, 0 for an empty payload. static uint32_t allocWritePayload(XdfT *x, const uint8_t *payload, size_t len) { uint32_t *chain; uint32_t need; uint32_t have; uint32_t c; uint32_t i; uint32_t head; size_t off; size_t chunk; need = (uint32_t)((len + x->clusterBytes - 1u) / x->clusterBytes); if (need == 0u) { return 0u; } have = freeCount(x); if (have < need) { toolFail("%s: need %u clusters, %u free (%zu bytes)", x->path, need, have, (size_t)have * x->clusterBytes); } chain = (uint32_t *)malloc(need * sizeof(uint32_t)); if (chain == NULL) { toolFail("out of memory"); } i = 0u; for (c = FIRST_CLUSTER; c <= x->maxCluster && i < need; c++) { if (fatGet(x, c) == 0u) { chain[i++] = c; } } for (i = 0; i < need; i++) { off = clusterOffset(x, chain[i]); chunk = len - (size_t)i * x->clusterBytes; if (chunk > x->clusterBytes) { chunk = x->clusterBytes; } memcpy(x->data + off, payload + (size_t)i * x->clusterBytes, chunk); memset(x->data + off + chunk, 0, x->clusterBytes - chunk); fatSet(x, chain[i], i == need - 1u ? EOC : chain[i + 1u]); } head = chain[0]; free(chain); return head; } static size_t clusterOffset(const XdfT *x, uint32_t cluster) { return x->dataStart + (size_t)(cluster - FIRST_CLUSTER) * x->clusterBytes; } // A root entry (of any kind) deleted, its chain freed; false when there is // none of that name. static bool deleteRoot(XdfT *x, const char *name) { size_t off; off = findEntry(x, 0u, name, strlen(name), false); if (off == NOT_FOUND) { return false; } freeChain(x, rd16(x->data + off + ENTRY_CLUSTER)); x->data[off] = DELETED_MARKER; return true; } // A root entry deleted with everything under it. A stock Human68k system // floppy keeps ~900 KB of utilities in folders (SYS, BIN, ...), which // deleting the root's files alone would leave allocated. static bool deleteTree(XdfT *x, const char *name) { NameT encoded; DirIterT it; size_t off; uint32_t cluster; nameEncode(name, strlen(name), &encoded); dirIterStart(&it, 0u); while ((off = dirNext(x, &it)) != NOT_FOUND && x->data[off] != FREE_MARKER) { if (x->data[off] == DELETED_MARKER || (x->data[off + ENTRY_ATTR] & ATTR_VOLUME) != 0u || !entryMatches(x->data + off, &encoded)) { continue; } cluster = rd16(x->data + off + ENTRY_CLUSTER); if ((x->data[off + ENTRY_ATTR] & ATTR_DIR) != 0u) { freeSubtree(x, cluster, 0u); } freeChain(x, cluster); x->data[off] = DELETED_MARKER; return true; } return false; } // A folder path ("DATA", "DATA/LEVELS") to its first cluster (the root is // 0), each folder made when missing and create is set; false when one is // missing and create is not. Folder names are 8 characters at most and // matched as written, upper-cased. static bool dirCluster(XdfT *x, const char *path, size_t pathLen, bool create, uint32_t *cluster) { uint8_t stem[ENTRY_NAME_BYTES]; uint8_t entry[DIR_ENTRY_SIZE]; uint32_t parent; uint32_t made; uint32_t found; size_t start; size_t end; size_t i; size_t off; bool have; DirIterT it; *cluster = 0u; for (start = 0; start < pathLen; start = end + 1u) { for (end = start; end < pathLen && path[end] != '/'; end++) { } if (end == start) { continue; } if (end - start > ENTRY_STEM_BYTES) { toolFail("subdir '%.*s' does not fit 8.3", (int)(end - start), path + start); } memset(stem, ' ', sizeof(stem)); for (i = start; i < end; i++) { if ((unsigned char)path[i] >= ASCII_END) { toolFail("'%.*s' is not ASCII", (int)pathLen, path); } stem[i - start] = (uint8_t)toupper((unsigned char)path[i]); } have = false; found = 0u; dirIterStart(&it, *cluster); while ((off = dirNext(x, &it)) != NOT_FOUND && x->data[off] != FREE_MARKER) { if (x->data[off] != DELETED_MARKER && (x->data[off + ENTRY_ATTR] & ATTR_DIR) != 0u && memcmp(x->data + off, stem, ENTRY_NAME_BYTES) == 0) { have = true; found = rd16(x->data + off + ENTRY_CLUSTER); break; } } if (have) { *cluster = found; continue; } if (!create) { return false; } if (freeCount(x) == 0u) { toolFail("%s: no free cluster for subdirectory", x->path); } parent = *cluster; made = freeNth(x, 0u); fatSet(x, made, EOC); off = clusterOffset(x, made); memset(x->data + off, 0, x->clusterBytes); dirEntry(x->data + off, kDotName, ATTR_DIR, made, 0u, NULL); dirEntry(x->data + off + DIR_ENTRY_SIZE, kDotDotName, ATTR_DIR, parent, 0u, NULL); dirEntry(entry, stem, ATTR_DIR, made, 0u, NULL); putEntryAny(x, parent, entry); *cluster = made; } return true; } // A 32-byte directory entry; extended NULL for none. static void dirEntry(uint8_t *out, const uint8_t *name11, uint32_t attr, uint32_t cluster, uint32_t size, const uint8_t *extended) { memset(out, 0, DIR_ENTRY_SIZE); memcpy(out, name11, ENTRY_NAME_BYTES); out[ENTRY_ATTR] = (uint8_t)attr; if (extended != NULL) { memcpy(out + ENTRY_EXTENDED, extended, ENTRY_EXTENDED_LEN); } wr16(out + ENTRY_TIME, ENTRY_TIME_VALUE); wr16(out + ENTRY_DATE, ENTRY_DATE_VALUE); wr16(out + ENTRY_CLUSTER, cluster); wr32(out + ENTRY_SIZE, size); } static void dirIterStart(DirIterT *it, uint32_t cluster) { it->cluster = cluster; it->index = 0u; it->guard = 0u; it->root = cluster == 0u; } // The next entry's offset, or NOT_FOUND past the last. static size_t dirNext(const XdfT *x, DirIterT *it) { uint32_t perCluster; size_t off; if (it->root) { if (it->index >= x->rootEntries) { return NOT_FOUND; } off = x->rootStart + (size_t)it->index * DIR_ENTRY_SIZE; it->index++; return off + DIR_ENTRY_SIZE <= x->len ? off : NOT_FOUND; } perCluster = (uint32_t)(x->clusterBytes / DIR_ENTRY_SIZE); if (it->index == perCluster) { it->cluster = fatGet(x, it->cluster); it->index = 0u; it->guard++; if (it->guard > x->maxCluster) { return NOT_FOUND; } } if (it->cluster < FIRST_CLUSTER || it->cluster >= EOC_MIN) { return NOT_FOUND; } off = clusterOffset(x, it->cluster) + (size_t)it->index * DIR_ENTRY_SIZE; it->index++; return off + DIR_ENTRY_SIZE <= x->len ? off : NOT_FOUND; } // Strip the image back to a formatted blank disk, its volume label kept: // every root entry and everything under it freed. A data floppy is made // this way (from Human68k's own disk) so its BPB and boot sector stay as // Human68k wrote them. static uint32_t emptyRoot(XdfT *x) { DirIterT it; size_t off; uint32_t count; uint32_t cluster; count = 0u; dirIterStart(&it, 0u); while ((off = dirNext(x, &it)) != NOT_FOUND && x->data[off] != FREE_MARKER) { if (x->data[off] == DELETED_MARKER || (x->data[off + ENTRY_ATTR] & ATTR_VOLUME) != 0u) { continue; } cluster = rd16(x->data + off + ENTRY_CLUSTER); if ((x->data[off + ENTRY_ATTR] & ATTR_DIR) != 0u) { freeSubtree(x, cluster, 0u); } freeChain(x, cluster); x->data[off] = DELETED_MARKER; count++; } return count; } // An entry holds name: its 8.3 field upper-cased equal, and its extended // stem, less trailing NULs and spaces, upper-cased equal. static bool entryMatches(const uint8_t *e, const NameT *name) { size_t i; size_t got; size_t want; for (i = 0; i < ENTRY_NAME_BYTES; i++) { if (toupper(e[i]) != name->main[i]) { return false; } } for (got = ENTRY_EXTENDED_LEN; got > 0u && (e[ENTRY_EXTENDED + got - 1u] == '\0' || e[ENTRY_EXTENDED + got - 1u] == ' '); got--) { } for (want = ENTRY_EXTENDED_LEN; want > 0u && (name->extended[want - 1u] == '\0' || name->extended[want - 1u] == ' '); want--) { } if (got != want) { return false; } for (i = 0; i < got; i++) { if (toupper(e[ENTRY_EXTENDED + i]) != name->extended[i]) { return false; } } return true; } // An entry's name as a listing shows it: stem (extended stem folded in) and // .ext, each without its trailing space, a non-ASCII byte shown as U+FFFD; // chars is how many characters that is. static void entryName(const uint8_t *e, char *out, size_t outSize, size_t *chars) { static const char replacement[] = "\xEF\xBF\xBD"; const uint8_t *nul; size_t ends[3]; size_t starts[3]; size_t part; size_t i; size_t n; size_t end; nul = (const uint8_t *)memchr(e + ENTRY_EXTENDED, '\0', ENTRY_EXTENDED_LEN); starts[0] = 0u; ends[0] = ENTRY_STEM_BYTES; starts[1] = ENTRY_EXTENDED; ends[1] = nul != NULL ? (size_t)(nul - e) : ENTRY_EXTENDED + ENTRY_EXTENDED_LEN; starts[2] = ENTRY_STEM_BYTES; ends[2] = ENTRY_NAME_BYTES; n = 0u; *chars = 0u; for (part = 0; part < 3u; part++) { end = ends[part]; while (end > starts[part] && isspace(e[end - 1u])) { end--; } if (part == 2u && end > starts[part] && n + 1u < outSize) { out[n++] = '.'; (*chars)++; } for (i = starts[part]; i < end && n + sizeof(replacement) < outSize; i++) { if (e[i] >= ASCII_END) { memcpy(out + n, replacement, sizeof(replacement) - 1u); n += sizeof(replacement) - 1u; } else { out[n++] = (char)e[i]; } (*chars)++; } } out[n] = '\0'; } static uint32_t fatGet(const XdfT *x, uint32_t cluster) { size_t off; uint32_t pair; off = x->fatStart + (size_t)cluster * 3u / 2u; if (off + 1u >= x->len) { return EOC; } pair = (uint32_t)x->data[off] | ((uint32_t)x->data[off + 1u] << 8); return (cluster & 1u) != 0u ? pair >> FAT12_HIGH_SHIFT : pair & FAT12_MASK; } // A FAT entry set in every FAT copy, so the image stays self-consistent. static void fatSet(XdfT *x, uint32_t cluster, uint32_t value) { uint32_t fat; uint32_t pair; size_t off; for (fat = 0; fat < x->nfats; fat++) { off = x->fatStart + (size_t)fat * x->spf * x->bps + (size_t)cluster * 3u / 2u; if (off + 1u >= x->len) { toolFail("%s: cluster %u is past the FAT", x->path, cluster); } pair = (uint32_t)x->data[off] | ((uint32_t)x->data[off + 1u] << 8); if ((cluster & 1u) != 0u) { pair = (pair & 0x000Fu) | ((value & FAT12_MASK) << FAT12_HIGH_SHIFT); } else { pair = (pair & 0xF000u) | (value & FAT12_MASK); } x->data[off] = (uint8_t)pair; x->data[off + 1u] = (uint8_t)(pair >> 8); } } // The offset of the first entry in a folder (0 the root) that holds name // (nameLen bytes), or NOT_FOUND; filesOnly passes over folders and volume // labels. The walk stops at a never-used entry. static size_t findEntry(const XdfT *x, uint32_t cluster, const char *name, size_t nameLen, bool filesOnly) { NameT encoded; DirIterT it; size_t off; nameEncode(name, nameLen, &encoded); dirIterStart(&it, cluster); while ((off = dirNext(x, &it)) != NOT_FOUND && x->data[off] != FREE_MARKER) { if (x->data[off] == DELETED_MARKER) { continue; } if (filesOnly && (x->data[off + ENTRY_ATTR] & (ATTR_VOLUME | ATTR_DIR)) != 0u) { continue; } if (entryMatches(x->data + off, &encoded)) { return off; } } return NOT_FOUND; } // A FAT chain released (a damaged image's looping chain stops). static void freeChain(XdfT *x, uint32_t cluster) { uint32_t next; uint32_t guard; for (guard = 0; cluster >= FIRST_CLUSTER && cluster < EOC_MIN && guard <= x->maxCluster; guard++) { next = fatGet(x, cluster); fatSet(x, cluster, 0u); cluster = next; } } static uint32_t freeCount(const XdfT *x) { uint32_t c; uint32_t n; n = 0u; for (c = FIRST_CLUSTER; c <= x->maxCluster; c++) { if (fatGet(x, c) == 0u) { n++; } } return n; } // The nth free cluster (0 the first), lowest first. static uint32_t freeNth(const XdfT *x, uint32_t n) { uint32_t c; for (c = FIRST_CLUSTER; c <= x->maxCluster; c++) { if (fatGet(x, c) == 0u) { if (n == 0u) { return c; } n--; } } return 0u; } // Every file and folder reachable from a folder's chain freed. "." and ".." // point back up, so they are passed over or the walk would eat the tree it // came from. static void freeSubtree(XdfT *x, uint32_t cluster, uint32_t depth) { DirIterT it; size_t off; size_t n; uint32_t child; if (depth > TREE_DEPTH_MAX) { return; } dirIterStart(&it, cluster); while ((off = dirNext(x, &it)) != NOT_FOUND && x->data[off] != FREE_MARKER) { if (x->data[off] == DELETED_MARKER || (x->data[off + ENTRY_ATTR] & ATTR_VOLUME) != 0u) { continue; } for (n = ENTRY_NAME_BYTES; n > 0u && x->data[off + n - 1u] == ' '; n--) { } if ((n == 1u && x->data[off] == '.') || (n == 2u && x->data[off] == '.' && x->data[off + 1u] == '.')) { continue; } child = rd16(x->data + off + ENTRY_CLUSTER); if ((x->data[off + ENTRY_ATTR] & ATTR_DIR) != 0u) { freeSubtree(x, child, depth + 1u); } freeChain(x, child); x->data[off] = DELETED_MARKER; } } // An image read whole, and its BPB. static void load(const char *path, XdfT *x) { memset(x, 0, sizeof(*x)); x->path = path; if (!toolReadFile(path, &x->data, &x->len)) { toolFail("cannot read %s", path); } if (x->len < BPB_END) { toolFail("%s: too short for a BPB", path); } x->bps = rd16(x->data + BPB_BPS); x->spc = x->data[BPB_SPC]; x->reserved = rd16(x->data + BPB_RESERVED); x->nfats = x->data[BPB_NFATS]; x->rootEntries = rd16(x->data + BPB_ROOT_ENTRIES); x->totalSectors = rd16(x->data + BPB_TOTAL_SECTORS); x->spf = rd16(x->data + BPB_SPF); if (x->bps == 0u || x->spc == 0u) { toolFail("%s: implausible BPB (bytesPerSector=%u)", path, x->bps); } x->fatStart = (size_t)x->reserved * x->bps; x->rootStart = ((size_t)x->reserved + (size_t)x->nfats * x->spf) * x->bps; x->dataStart = x->rootStart + (size_t)x->rootEntries * DIR_ENTRY_SIZE; x->clusterBytes = (size_t)x->spc * x->bps; // Clusters are numbered from 2; the last is bounded by the room left // after the root directory. x->maxCluster = 1u + (x->len > x->dataStart ? (uint32_t)((x->len - x->dataStart) / x->clusterBytes) : 0u); } // A name (len bytes) as an entry holds it, upper-cased: an 18.3 Human68k // name, the stem's first 8 characters in the 8.3 field and the rest in the // extended stem. A fatal error when it does not fit or is not ASCII. static void nameEncode(const char *name, size_t len, NameT *out) { const char *dot; size_t stemLen; size_t extLen; size_t i; for (i = 0; i < len; i++) { if ((unsigned char)name[i] >= ASCII_END) { toolFail("'%.*s' is not ASCII", (int)len, name); } } dot = memchr(name, '.', len); stemLen = dot != NULL ? (size_t)(dot - name) : len; extLen = dot != NULL ? len - stemLen - 1u : 0u; if (stemLen > STEM_MAX || extLen > ENTRY_EXT_BYTES) { toolFail("'%.*s' does not fit Human68k 18.3", (int)len, name); } memset(out->main, ' ', sizeof(out->main)); memset(out->extended, 0, sizeof(out->extended)); for (i = 0; i < stemLen; i++) { if (i < ENTRY_STEM_BYTES) { out->main[i] = (uint8_t)toupper((unsigned char)name[i]); } else { out->extended[i - ENTRY_STEM_BYTES] = (uint8_t)toupper((unsigned char)name[i]); } } for (i = 0; i < extLen; i++) { out->main[ENTRY_STEM_BYTES + i] = (uint8_t)toupper((unsigned char)dot[1 + i]); } } // An entry into a folder: the root's first free slot (a fixed table), or // putEntryInDir's. static void putEntryAny(XdfT *x, uint32_t cluster, const uint8_t *entry) { DirIterT it; size_t off; if (cluster != 0u) { putEntryInDir(x, cluster, entry); return; } dirIterStart(&it, 0u); while ((off = dirNext(x, &it)) != NOT_FOUND) { if (x->data[off] == FREE_MARKER || x->data[off] == DELETED_MARKER) { memcpy(x->data + off, entry, DIR_ENTRY_SIZE); return; } } toolFail("%s: root directory full", x->path); } // An entry into the first free or deleted slot of the folder whose chain // starts at firstCluster, the chain grown by a fresh cluster when every slot // is used. static void putEntryInDir(XdfT *x, uint32_t firstCluster, const uint8_t *entry) { uint32_t perCluster; uint32_t cluster; uint32_t last; uint32_t guard; uint32_t made; uint32_t i; size_t slot; perCluster = (uint32_t)(x->clusterBytes / DIR_ENTRY_SIZE); cluster = firstCluster; last = cluster; for (guard = 0; cluster >= FIRST_CLUSTER && cluster < EOC_MIN; guard++) { if (guard > x->maxCluster) { toolFail("%s: directory cluster chain loops", x->path); } for (i = 0; i < perCluster; i++) { slot = clusterOffset(x, cluster) + (size_t)i * DIR_ENTRY_SIZE; if (x->data[slot] == FREE_MARKER || x->data[slot] == DELETED_MARKER) { memcpy(x->data + slot, entry, DIR_ENTRY_SIZE); return; } } last = cluster; cluster = fatGet(x, cluster); } if (freeCount(x) == 0u) { toolFail("%s: no free cluster to extend directory", x->path); } made = freeNth(x, 0u); fatSet(x, last, made); fatSet(x, made, EOC); slot = clusterOffset(x, made); memset(x->data + slot, 0, x->clusterBytes); memcpy(x->data + slot, entry, DIR_ENTRY_SIZE); } static uint16_t rd16(const uint8_t *p) { return (uint16_t)(p[0] | (p[1] << 8)); } static uint32_t rd32(const uint8_t *p) { return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); } // A file's bytes: NAME in the root, SUB/NAME in a folder of the root, or a // deeper path. A fatal error when it is not there. static uint8_t *readFile(XdfT *x, const char *name, size_t *len) { const char *slash; const char *last; uint8_t *out; uint32_t cluster; uint32_t guard; size_t off; size_t size; size_t chunk; size_t have; slash = strchr(name, '/'); last = strrchr(name, '/'); off = NOT_FOUND; if (slash != NULL && slash != last) { // DATA/LEVELS/X.DAT: walk the whole chain. if (dirCluster(x, name, (size_t)(last - name), false, &cluster)) { off = findEntry(x, cluster, last + 1, strlen(last + 1), true); } } else if (slash != NULL) { // SAVES/RN.CFG: through the folder of the root. off = findEntry(x, 0u, name, (size_t)(slash - name), false); if (off != NOT_FOUND && (x->data[off + ENTRY_ATTR] & ATTR_DIR) != 0u) { off = findEntry(x, rd16(x->data + off + ENTRY_CLUSTER), slash + 1, strlen(slash + 1), true); } else { off = NOT_FOUND; } } else { off = findEntry(x, 0u, name, strlen(name), false); } if (off == NOT_FOUND) { toolFail("%s not in %s", name, x->path); } size = rd32(x->data + off + ENTRY_SIZE); cluster = rd16(x->data + off + ENTRY_CLUSTER); out = (uint8_t *)malloc(size + 1u); if (out == NULL) { toolFail("out of memory"); } have = 0u; for (guard = 0; cluster >= FIRST_CLUSTER && cluster < EOC_MIN && have < size; guard++) { if (guard >= x->maxCluster) { toolFail("%s: cluster chain loops", name); } off = clusterOffset(x, cluster); chunk = size - have < x->clusterBytes ? size - have : x->clusterBytes; if (off >= x->len) { break; } if (off + chunk > x->len) { chunk = x->len - off; } memcpy(out + have, x->data + off, chunk); have += chunk; cluster = fatGet(x, cluster); } *len = have; return out; } static void save(const XdfT *x) { if (!toolWriteFile(x->path, x->data, x->len)) { toolFail("cannot write %s", x->path); } } static void usage(void) { fprintf(stdout, "usage:\n"); fprintf(stdout, " %s list IMAGE\n", TOOL_NAME); fprintf(stdout, " %s add IMAGE HOSTFILE [NAME] (NAME may be SUB/NAME or deeper)\n", TOOL_NAME); fprintf(stdout, " %s delete IMAGE NAME\n", TOOL_NAME); fprintf(stdout, " %s deltree IMAGE NAME\n", TOOL_NAME); fprintf(stdout, " %s empty IMAGE\n", TOOL_NAME); fprintf(stdout, " %s extract IMAGE NAME HOSTFILE\n", TOOL_NAME); fprintf(stdout, " %s free IMAGE\n", TOOL_NAME); exit(USAGE_EXIT); } static void wr16(uint8_t *p, uint32_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } static void wr32(uint8_t *p, uint32_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); } int main(int argc, char **argv) { const char *cmd; const char *image; const char *name; const char *slash; const char *last; uint8_t *payload; size_t len; size_t chars; uint32_t n; char dirName[NAME_TEXT_MAX]; char text[NAME_TEXT_MAX]; DirIterT it; size_t off; XdfT x; toolFileName(TOOL_NAME); if (argc < 3) { usage(); } cmd = argv[1]; image = argv[2]; if (strcmp(cmd, "list") == 0) { load(image, &x); printf("%s: %zu bytes, %u-byte sectors, %u sectors, %u sec/cluster\n", image, x.len, x.bps, x.totalSectors, x.spc); dirIterStart(&it, 0u); while ((off = dirNext(&x, &it)) != NOT_FOUND && x.data[off] != FREE_MARKER) { if (x.data[off] == DELETED_MARKER || (x.data[off + ENTRY_ATTR] & (ATTR_VOLUME | ATTR_DIR)) != 0u) { continue; } entryName(x.data + off, text, sizeof(text), &chars); printf(" %s%*s %9u clus=%u\n", text, chars < LIST_NAME_WIDTH ? (int)(LIST_NAME_WIDTH - chars) : 0, "", rd32(x.data + off + ENTRY_SIZE), rd16(x.data + off + ENTRY_CLUSTER)); } return 0; } if (strcmp(cmd, "free") == 0) { load(image, &x); n = freeCount(&x); printf("%zu bytes free (%u clusters)\n", (size_t)n * x.clusterBytes, n); return 0; } if (strcmp(cmd, "add") == 0) { if (argc < 4) { fprintf(stderr, "usage: %s add [name]\n", TOOL_NAME); return USAGE_EXIT; } if (argc > 4) { name = argv[4]; } else { name = strrchr(argv[3], '/') != NULL ? strrchr(argv[3], '/') + 1 : argv[3]; } load(image, &x); if (!toolReadFile(argv[3], &payload, &len)) { toolFail("cannot read %s", argv[3]); } slash = strchr(name, '/'); last = strrchr(name, '/'); if (slash != NULL && slash != last) { addFileAtPath(&x, name, payload, len); } else if (slash != NULL) { if ((size_t)(slash - name) >= sizeof(dirName)) { toolFail("subdir '%.*s' does not fit 8.3", (int)(slash - name), name); } memcpy(dirName, name, (size_t)(slash - name)); dirName[slash - name] = '\0'; addFileInSubdir(&x, dirName, slash + 1, payload, len); } else { addFile(&x, name, payload, len); } save(&x); printf("added %s (%zu bytes) to %s\n", name, len, image); return 0; } if (strcmp(cmd, "delete") == 0 || strcmp(cmd, "deltree") == 0) { if (argc < 4) { fprintf(stderr, "usage: %s %s \n", TOOL_NAME, cmd); return USAGE_EXIT; } load(image, &x); if (!(strcmp(cmd, "delete") == 0 ? deleteRoot(&x, argv[3]) : deleteTree(&x, argv[3]))) { fprintf(stderr, "%s not in %s\n", argv[3], image); return 1; } save(&x); printf("deleted %s%s from %s\n", strcmp(cmd, "delete") == 0 ? "" : "tree ", argv[3], image); return 0; } if (strcmp(cmd, "empty") == 0) { load(image, &x); n = emptyRoot(&x); save(&x); printf("emptied %s (%u root entries removed)\n", image, n); return 0; } if (strcmp(cmd, "extract") == 0) { if (argc < 5) { fprintf(stderr, "usage: %s extract \n", TOOL_NAME); return USAGE_EXIT; } load(image, &x); payload = readFile(&x, argv[3], &len); if (!toolWriteFile(argv[4], payload, len)) { toolFail("cannot write %s", argv[4]); } printf("extracted %s -> %s\n", argv[3], argv[4]); return 0; } fprintf(stderr, "unknown command '%s'\n", cmd); return USAGE_EXIT; }