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