1062 lines
36 KiB
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;
|
|
}
|