264 lines
10 KiB
C
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;
|
|
}
|