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