419 lines
14 KiB
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;
|
|
}
|