fs2port/port/tools/prodosextract.c
2026-05-13 21:32:05 -05:00

264 lines
10 KiB
C

// Minimal ProDOS volume reader.
//
// Walks the volume directory, follows index/master-index blocks for
// sapling/tree files, and writes each file out as a flat binary. Big
// enough to handle the FS2 "san inc pack" image (which ships all the
// scenery as ProDOS files), small enough to live in tools/.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define BLOCK_SIZE 512
#define ENTRY_SIZE 39
#define ENTRIES_PER_BLOCK 13
static const uint8_t *image;
static size_t imageSize;
typedef struct EntryT {
uint8_t storageTypeNameLen;
char name[16];
uint8_t fileType;
uint16_t keyPointer;
uint16_t blocksUsed;
uint32_t eof;
uint16_t auxType;
} EntryT;
static int extractFile(const EntryT *e, const char *outDir);
static const uint8_t *blockAt(uint16_t block);
static int parseEntry(const uint8_t *raw, EntryT *out);
static int walkVolume(const char *outDir);
static int writeBlocks(FILE *out, FILE *blockList, uint16_t keyBlock, uint8_t storageType, uint32_t eof);
static int writeSapling(FILE *out, FILE *blockList, uint16_t indexBlock, uint32_t eof);
static int writeTree(FILE *out, FILE *blockList, uint16_t masterBlock, uint32_t eof);
static const uint8_t *blockAt(uint16_t block) {
size_t off = (size_t)block * BLOCK_SIZE;
if (off + BLOCK_SIZE > imageSize) {
fprintf(stderr, "block %u out of range\n", block);
return NULL;
}
return image + off;
}
static int parseEntry(const uint8_t *raw, EntryT *out) {
memset(out, 0, sizeof(*out));
out->storageTypeNameLen = raw[0];
uint8_t storageType = (uint8_t)(raw[0] >> 4);
uint8_t nameLen = (uint8_t)(raw[0] & 0x0F);
if (storageType == 0 || nameLen == 0 || nameLen > 15) {
return 0;
}
memcpy(out->name, &raw[1], nameLen);
out->name[nameLen] = '\0';
out->fileType = raw[0x10];
out->keyPointer = (uint16_t)(raw[0x11] | (raw[0x12] << 8));
out->blocksUsed = (uint16_t)(raw[0x13] | (raw[0x14] << 8));
out->eof = (uint32_t)(raw[0x15]) | ((uint32_t)raw[0x16] << 8) | ((uint32_t)raw[0x17] << 16);
out->auxType = (uint16_t)(raw[0x1F] | (raw[0x20] << 8));
return 1;
}
// Walk a sapling (storage_type == 2): key_pointer points at one index
// block containing 256 16-bit data-block pointers (lo bytes 0..255,
// then hi bytes 256..511). Sparse data blocks (pointer = 0) are
// represented as zero-filled output bytes. If `blockList` is non-NULL,
// each block number is also appended to that file as a 16-bit
// little-endian word -- producing an in-order list of every ProDOS
// block this file occupies on disk.
static int writeSapling(FILE *out, FILE *blockList, uint16_t indexBlock, uint32_t eof) {
const uint8_t *idx = blockAt(indexBlock);
if (idx == NULL) {
return 0;
}
uint32_t remaining = eof;
for (int i = 0; i < 256 && remaining > 0; i++) {
uint16_t blk = (uint16_t)(idx[i] | (idx[i + 256] << 8));
uint32_t chunk = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining;
if (blockList != NULL) {
uint8_t entry[2] = { (uint8_t)(blk & 0xFF), (uint8_t)((blk >> 8) & 0xFF) };
fwrite(entry, 1, 2, blockList);
}
if (blk == 0) {
static const uint8_t zeros[BLOCK_SIZE] = { 0 };
fwrite(zeros, 1, chunk, out);
} else {
const uint8_t *data = blockAt(blk);
if (data == NULL) {
return 0;
}
fwrite(data, 1, chunk, out);
}
remaining -= chunk;
}
return 1;
}
// Walk a tree (storage_type == 3): key_pointer is a master index
// block. Empirically the master index uses the same lo[0..255] /
// hi[256..511] layout as a regular sapling index block (Beneath Apple
// ProDOS describes a +128 split for master indexes, but the volumes
// I have in hand all use +256 -- the FS2 "san inc pack" image and
// every other ProDOS image I tested).
static int writeTree(FILE *out, FILE *blockList, uint16_t masterBlock, uint32_t eof) {
const uint8_t *master = blockAt(masterBlock);
if (master == NULL) {
return 0;
}
uint32_t remaining = eof;
for (int i = 0; i < 128 && remaining > 0; i++) {
uint16_t indexBlk = (uint16_t)(master[i] | (master[i + 256] << 8));
uint32_t chunkMax = (uint32_t)256 * BLOCK_SIZE;
uint32_t chunk = remaining > chunkMax ? chunkMax : remaining;
if (indexBlk == 0) {
// Whole subtree is sparse -> output zeros and
// record zero block-numbers for the gap.
for (uint32_t z = 0; z < chunk; z++) {
fputc(0, out);
}
if (blockList != NULL) {
uint32_t blocksInGap = (chunk + BLOCK_SIZE - 1) / BLOCK_SIZE;
for (uint32_t z = 0; z < blocksInGap; z++) {
uint8_t zero[2] = { 0, 0 };
fwrite(zero, 1, 2, blockList);
}
}
} else {
if (!writeSapling(out, blockList, indexBlk, chunk)) {
return 0;
}
}
remaining -= chunk;
}
return 1;
}
static int writeBlocks(FILE *out, FILE *blockList, uint16_t keyBlock, uint8_t storageType, uint32_t eof) {
switch (storageType) {
case 1: {
const uint8_t *data = blockAt(keyBlock);
if (data == NULL) {
return 0;
}
if (blockList != NULL) {
uint8_t entry[2] = { (uint8_t)(keyBlock & 0xFF), (uint8_t)((keyBlock >> 8) & 0xFF) };
fwrite(entry, 1, 2, blockList);
}
fwrite(data, 1, eof > BLOCK_SIZE ? BLOCK_SIZE : eof, out);
return 1;
}
case 2:
return writeSapling(out, blockList, keyBlock, eof);
case 3:
return writeTree(out, blockList, keyBlock, eof);
default:
fprintf(stderr, "unsupported storage type %u\n", storageType);
return 0;
}
}
static int extractFile(const EntryT *e, const char *outDir) {
char outPath[1024];
char blockListPath[1024];
snprintf(outPath, sizeof(outPath), "%s/%s", outDir, e->name);
snprintf(blockListPath, sizeof(blockListPath), "%s/%s.blocks", outDir, e->name);
FILE *out = fopen(outPath, "wb");
FILE *blks = fopen(blockListPath, "wb");
if (out == NULL || blks == NULL) {
fprintf(stderr, "cannot open %s / %s\n", outPath, blockListPath);
if (out != NULL) fclose(out);
if (blks != NULL) fclose(blks);
return 0;
}
uint8_t storageType = (uint8_t)(e->storageTypeNameLen >> 4);
int ok = writeBlocks(out, blks, e->keyPointer, storageType, e->eof);
fclose(out);
fclose(blks);
printf(" %-16s type=$%02X aux=$%04X blocks=%u eof=%u storage=%u%s\n",
e->name, e->fileType, e->auxType, e->blocksUsed, e->eof, storageType,
ok ? "" : " (FAILED)");
return ok;
}
static int walkVolume(const char *outDir) {
// Volume directory starts at block 2.
uint16_t blockNum = 2;
int isHeader = 1;
int total = 0;
while (blockNum != 0) {
const uint8_t *blk = blockAt(blockNum);
if (blk == NULL) {
return 0;
}
uint16_t nextBlock = (uint16_t)(blk[2] | (blk[3] << 8));
int offset = 4;
if (isHeader) {
// First entry of block 2 is the volume header,
// not a regular file entry.
uint8_t volNameLen = (uint8_t)(blk[4] & 0x0F);
char volName[16];
memcpy(volName, &blk[5], volNameLen);
volName[volNameLen] = '\0';
printf("Volume: /%s/\n", volName);
offset = 4 + ENTRY_SIZE;
isHeader = 0;
}
while (offset + ENTRY_SIZE <= 4 + ENTRIES_PER_BLOCK * ENTRY_SIZE) {
EntryT entry;
if (parseEntry(&blk[offset], &entry)) {
if (extractFile(&entry, outDir)) {
total++;
}
}
offset += ENTRY_SIZE;
}
blockNum = nextBlock;
}
printf("\nextracted %d file(s) -> %s\n", total, outDir);
return total;
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "usage: %s in.po out_dir\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "rb");
if (f == NULL) {
fprintf(stderr, "cannot open %s\n", argv[1]);
return 1;
}
fseek(f, 0, SEEK_END);
imageSize = (size_t)ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buf = malloc(imageSize);
if (buf == NULL || fread(buf, 1, imageSize, f) != imageSize) {
fprintf(stderr, "could not read %s\n", argv[1]);
fclose(f);
free(buf);
return 1;
}
fclose(f);
image = buf;
mkdir(argv[2], 0755);
int ok = walkVolume(argv[2]);
free(buf);
return ok > 0 ? 0 : 1;
}