joeylib2/tools/xdftool/xdftool.c

1062 lines
36 KiB
C

// 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 <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <image> <hostfile> [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 <image> <name>\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 <image> <name> <hostfile>\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;
}