joeylib2/tools/adfTool/adfTool.c

419 lines
14 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// adfTool.c - read files out of an Amiga OFS/FFS disk image (.adf).
//
// adfTool list IMAGE every file, one path per line
// adfTool extract IMAGE PATH OUT one file (PATH like Devs/serial.device)
// adfTool extractall IMAGE OUTDIR every file, keeping the drawer tree
//
// Read-only. Used by the installer's `import` (a Workbench disk image is how
// most people own AmigaOS files such as Devs/serial.device) and by the Amiga
// gates. Handles DD (880 KB) and HD (1.76 MB) images; names match
// case-insensitively, as AmigaDOS does, and a drawer lists in name order
// without regard to case.
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "toolBuf.h"
#include "toolFile.h"
#define TOOL_NAME "adfTool"
#define USAGE_EXIT 2
#define BLOCK_SIZE 512u
// The hash table (and a file header's table of data blocks) has 72 slots
// from byte 24.
#define HASH_SLOTS 72u
#define TABLE_AT 24u
#define TYPE_HEADER 2
#define SEC_TYPE_USERDIR 2
#define SEC_TYPE_FILE (-3)
// OFS data blocks start with a 24-byte header, their data's length at 12.
#define OFS_DATA_HEADER 24u
#define OFS_DATA_SIZE_AT 12
// Fields counted from a header block's end, as AmigaDOS defines them: the
// name's length byte (its characters after it), the file's size, the hash
// chain's next, the extension block, and the secondary type.
#define NAME_LENGTH_FROM_END 80
#define FILE_SIZE_FROM_END 188
#define HASH_CHAIN_FROM_END 16
#define EXTENSION_FROM_END 8
#define SEC_TYPE_FROM_END 4
#define BLOCK_COUNT_AT 8
#define NAME_MAX_BYTES 31u
// A name is Latin-1; on the host it is UTF-8, up to two bytes a character.
#define NAME_UTF8_BYTES (2u * NAME_MAX_BYTES + 1u)
#define ASCII_END 0x80u
#define UTF8_LEAD_2 0xC0u
#define UTF8_CONT 0x80u
#define UTF8_SHIFT 6u
#define UTF8_CONT_MASK 0x3Fu
#define PATH_MAX_BYTES 1024u
#define DIR_MODE 0777
#define DOS_MAGIC_BYTES 3u
#define FFS_FLAG 1u
// One entry of a drawer: its name, header block, and secondary type.
typedef struct EntryT {
char name[NAME_UTF8_BYTES];
uint32_t block;
int32_t secType;
} EntryT;
typedef struct AdfT {
const char *path;
uint8_t *image;
size_t len;
uint32_t blocks;
uint32_t rootBlock;
bool ffs;
} AdfT;
static const uint8_t *block(const AdfT *a, uint32_t number);
static size_t entries(const AdfT *a, uint32_t dirBlock, EntryT **out);
static int entryCompare(const void *x, const void *y);
static void extractAll(const AdfT *a, uint32_t dirBlock, const char *prefix, const char *outDir);
static int32_t field(const uint8_t *data, int offset);
static uint32_t find(const AdfT *a, const char *path);
static void latin1ToUtf8(const uint8_t *name, size_t len, char *out);
static void listAll(const AdfT *a, uint32_t dirBlock, const char *prefix);
static void makeDirs(const char *path);
static bool nameEqual(const char *a, size_t aLen, const char *b, size_t bLen);
static uint8_t *readFile(const AdfT *a, uint32_t headerBlock, size_t *len);
static void usage(void);
// A block of the image; a fatal error past its end.
static const uint8_t *block(const AdfT *a, uint32_t number) {
if (number >= a->blocks) {
toolFail("%s: block %u is past the image's end", a->path, number);
}
return a->image + (size_t)number * BLOCK_SIZE;
}
// Everything in a drawer (its hash table's chains), in name order; how
// many. A damaged image's looping chain stops after as many entries as the
// image has blocks.
static size_t entries(const AdfT *a, uint32_t dirBlock, EntryT **out) {
const uint8_t *dir;
const uint8_t *header;
EntryT *found;
EntryT *grown;
size_t count;
size_t cap;
size_t nameLen;
uint32_t slot;
uint32_t number;
uint32_t guard;
dir = block(a, dirBlock);
found = NULL;
count = 0u;
cap = 0u;
guard = 0u;
for (slot = 0; slot < HASH_SLOTS; slot++) {
number = (uint32_t)field(dir, (int)(TABLE_AT + 4u * slot));
while (number != 0u) {
if (++guard > a->blocks) {
toolFail("%s: a drawer's hash chain loops", a->path);
}
header = block(a, number);
if (field(header, 0) == TYPE_HEADER) {
if (count == cap) {
cap = cap == 0u ? 16u : cap * 2u;
grown = (EntryT *)realloc(found, cap * sizeof(EntryT));
if (grown == NULL) {
toolFail("out of memory");
}
found = grown;
}
nameLen = header[BLOCK_SIZE - NAME_LENGTH_FROM_END];
if (nameLen > NAME_MAX_BYTES) {
nameLen = NAME_MAX_BYTES;
}
latin1ToUtf8(header + BLOCK_SIZE - NAME_LENGTH_FROM_END + 1u, nameLen, found[count].name);
found[count].block = number;
found[count].secType = field(header, -SEC_TYPE_FROM_END);
count++;
}
number = (uint32_t)field(header, -HASH_CHAIN_FROM_END);
}
}
if (count > 1u) {
qsort(found, count, sizeof(EntryT), entryCompare);
}
*out = found;
return count;
}
// Names in order without regard to (ASCII) case.
static int entryCompare(const void *x, const void *y) {
const unsigned char *p;
const unsigned char *q;
p = (const unsigned char *)((const EntryT *)x)->name;
q = (const unsigned char *)((const EntryT *)y)->name;
while (*p != '\0' && toupper(*p) == toupper(*q)) {
p++;
q++;
}
return toupper(*p) - toupper(*q);
}
// Every file under a drawer into outDir, keeping the drawer tree.
static void extractAll(const AdfT *a, uint32_t dirBlock, const char *prefix, const char *outDir) {
EntryT *list;
uint8_t *data;
size_t count;
size_t len;
size_t i;
char path[PATH_MAX_BYTES];
char out[PATH_MAX_BYTES];
count = entries(a, dirBlock, &list);
for (i = 0; i < count; i++) {
snprintf(path, sizeof(path), "%s%s", prefix, list[i].name);
if (list[i].secType == SEC_TYPE_USERDIR) {
strncat(path, "/", sizeof(path) - strlen(path) - 1u);
extractAll(a, list[i].block, path, outDir);
} else if (list[i].secType == SEC_TYPE_FILE) {
snprintf(out, sizeof(out), "%s/%s", outDir, prefix);
makeDirs(out);
strncat(out, list[i].name, sizeof(out) - strlen(out) - 1u);
data = readFile(a, list[i].block, &len);
if (!toolWriteFile(out, data, len)) {
toolFail("cannot write %s", out);
}
free(data);
}
}
free(list);
}
// A big-endian 32-bit field of a block; a negative offset counts from the
// block's end, as the AmigaDOS structure definitions do.
static int32_t field(const uint8_t *data, int offset) {
const uint8_t *p;
p = data + (offset < 0 ? (int)BLOCK_SIZE + offset : offset);
return (int32_t)(((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]);
}
// The header block of the file or drawer at path ("Devs/serial.device"),
// each part matched without regard to case; a fatal error when it is not
// there.
static uint32_t find(const AdfT *a, const char *path) {
EntryT *list;
const char *part;
const char *end;
uint32_t current;
size_t count;
size_t i;
current = a->rootBlock;
for (part = path;; part = end + 1) {
end = strchr(part, '/');
end = end != NULL ? end : part + strlen(part);
count = entries(a, current, &list);
for (i = 0; i < count && !nameEqual(list[i].name, strlen(list[i].name), part, (size_t)(end - part)); i++) {
}
if (i == count) {
toolFail("not found in image: %s", path);
}
current = list[i].block;
free(list);
if (*end == '\0') {
break;
}
}
return current;
}
// len bytes of a Latin-1 name as a UTF-8 string in out (NAME_UTF8_BYTES).
static void latin1ToUtf8(const uint8_t *name, size_t len, char *out) {
size_t i;
size_t n;
n = 0u;
for (i = 0; i < len; i++) {
if (name[i] < ASCII_END) {
out[n++] = (char)name[i];
} else {
out[n++] = (char)(UTF8_LEAD_2 | (name[i] >> UTF8_SHIFT));
out[n++] = (char)(UTF8_CONT | (name[i] & UTF8_CONT_MASK));
}
}
out[n] = '\0';
}
// Every file under a drawer, one path a line.
static void listAll(const AdfT *a, uint32_t dirBlock, const char *prefix) {
EntryT *list;
size_t count;
size_t i;
char path[PATH_MAX_BYTES];
count = entries(a, dirBlock, &list);
for (i = 0; i < count; i++) {
snprintf(path, sizeof(path), "%s%s", prefix, list[i].name);
if (list[i].secType == SEC_TYPE_USERDIR) {
strncat(path, "/", sizeof(path) - strlen(path) - 1u);
listAll(a, list[i].block, path);
} else if (list[i].secType == SEC_TYPE_FILE) {
printf("%s\n", path);
}
}
free(list);
}
// A folder and every folder above it that is not there yet.
static void makeDirs(const char *path) {
char buf[PATH_MAX_BYTES];
size_t i;
size_t n;
n = strlen(path);
if (n >= sizeof(buf)) {
toolFail("path too long: %s", path);
}
memcpy(buf, path, n + 1u);
for (i = 1; i <= n; i++) {
if (buf[i] == '/' || buf[i] == '\0') {
buf[i] = '\0';
if (mkdir(buf, DIR_MODE) != 0 && errno != EEXIST) {
toolFail("cannot make the folder %s: %s", buf, strerror(errno));
}
buf[i] = path[i];
}
}
}
static bool nameEqual(const char *a, size_t aLen, const char *b, size_t bLen) {
size_t i;
if (aLen != bLen) {
return false;
}
for (i = 0; i < aLen; i++) {
if (toupper((unsigned char)a[i]) != toupper((unsigned char)b[i])) {
return false;
}
}
return true;
}
// A file's bytes: its header's and extension blocks' tables of data
// blocks, which fill from the end (slot 71 first); an OFS data block holds
// its length after a 24-byte header, an FFS one is all data.
static uint8_t *readFile(const AdfT *a, uint32_t headerBlock, size_t *len) {
const uint8_t *chunk;
const uint8_t *data;
ToolBufT out;
int32_t size;
int32_t count;
int32_t used;
uint32_t slot;
uint32_t extension;
uint32_t guard;
memset(&out, 0, sizeof(out));
chunk = block(a, headerBlock);
size = field(chunk, -FILE_SIZE_FROM_END);
for (guard = 0;; guard++) {
if (guard > a->blocks) {
toolFail("%s: a file's extension blocks loop", a->path);
}
count = field(chunk, BLOCK_COUNT_AT);
for (slot = 0; (int32_t)slot < count && slot < HASH_SLOTS; slot++) {
data = block(a, (uint32_t)field(chunk, (int)(TABLE_AT + 4u * (HASH_SLOTS - 1u - slot))));
if (a->ffs) {
toolBufAdd(&out, data, BLOCK_SIZE);
} else {
used = field(data, OFS_DATA_SIZE_AT);
if (used < 0) {
used = 0;
} else if ((uint32_t)used > BLOCK_SIZE - OFS_DATA_HEADER) {
used = (int32_t)(BLOCK_SIZE - OFS_DATA_HEADER);
}
toolBufAdd(&out, data + OFS_DATA_HEADER, (size_t)used);
}
}
extension = (uint32_t)field(chunk, -EXTENSION_FROM_END);
if (extension == 0u) {
break;
}
chunk = block(a, extension);
}
*len = size < 0 ? 0u : ((size_t)size < out.len ? (size_t)size : out.len);
if (out.data == NULL) {
toolBufZeros(&out, 1u);
}
return out.data;
}
static void usage(void) {
fprintf(stderr, "usage: %s list IMAGE\n", TOOL_NAME);
fprintf(stderr, " %s extract IMAGE PATH OUT\n", TOOL_NAME);
fprintf(stderr, " %s extractall IMAGE OUTDIR\n", TOOL_NAME);
exit(USAGE_EXIT);
}
int main(int argc, char **argv) {
AdfT a;
uint8_t *data;
size_t len;
toolFileName(TOOL_NAME);
if (argc < 3 || (strcmp(argv[1], "list") != 0 && strcmp(argv[1], "extract") != 0 && strcmp(argv[1], "extractall") != 0)) {
usage();
}
memset(&a, 0, sizeof(a));
a.path = argv[2];
if (!toolReadFile(a.path, &a.image, &a.len)) {
toolFail("cannot read %s", a.path);
}
if (a.len % BLOCK_SIZE != 0u || a.len < BLOCK_SIZE || memcmp(a.image, "DOS", DOS_MAGIC_BYTES) != 0) {
toolFail("%s is not an AmigaDOS disk image", a.path);
}
a.ffs = (a.image[DOS_MAGIC_BYTES] & FFS_FLAG) != 0u;
a.blocks = (uint32_t)(a.len / BLOCK_SIZE);
a.rootBlock = a.blocks / 2u;
if (strcmp(argv[1], "list") == 0) {
listAll(&a, a.rootBlock, "");
} else if (strcmp(argv[1], "extract") == 0) {
if (argc != 5) {
usage();
}
data = readFile(&a, find(&a, argv[3]), &len);
if (!toolWriteFile(argv[4], data, len)) {
toolFail("cannot write %s", argv[4]);
}
free(data);
} else {
if (argc != 4) {
usage();
}
extractAll(&a, a.rootBlock, "", argv[3]);
}
free(a.image);
return 0;
}