/* ========================================================================= * MZQDTool — Sharp MZ-700 Quick Disk (QD) image tool * * Creates and manages QD disk images for the MZ-700 Quick Disk system. * Supports format, directory listing, and adding MZF files to QD images. * * Original V0.30 (c) 2002 by BKK * Updated V1.00 (c) 2026 Philip Smart — rewritten with configurable disk * name, fixed file ordering, correct CMT→QD header conversion, * and robust error handling. * * QD disk image format (61455 bytes): * * Disk header: * [00] [16 16] [A5] [block_count] [CRC: 43 52 43] * * Per file (2 blocks each: header + data): * * Header block: * [00] [16 16] [A5] [00] [size_lo=40] [size_hi=00] * [64 bytes: QDHeaderStr] * [CRC: 43 52 43] * * Data block: * [00] [16 16] [A5] [05] [size_lo] [size_hi] * [file data] * [CRC: 43 52 43] * * Usage: * MZQDTool format [-o disk.qd] * MZQDTool dir [-o disk.qd] * MZQDTool add [-o disk.qd] * ========================================================================= */ #define VERSION "1.00" #include #include #include #include #define QDSIZE 61455 #define MAXQDFILETYPES 12 #define DEFAULTQDFILE "MZ700.qd" #define CMTHDRSIZE 128 /* MZF/CMT file header size */ #define QDHDRSIZE 64 /* QD file header size */ /* MZF/CMT tape file header (128 bytes) */ struct CMTHeaderStr { uint8_t Attribute; /* File type: 01=OBJ, 02=BTX, etc. */ char Name[17]; /* Filename (CR-terminated, null-padded) */ uint16_t Size; /* Data size (0 = use actual file size) */ uint16_t LoadAddress; /* Load address (DTADR) */ uint16_t ExecAddress; /* Execution address (EXADR) */ char Comment[104]; /* Comment area */ }; /* QD file header (64 bytes, stored in header block) */ struct QDHeaderStr { uint8_t Attribute; /* File type: 01=OBJ, 02=BTX, etc. */ char Name[17]; /* Filename (CR-terminated, null-padded) */ uint16_t Unknown; /* Reserved (usually 0) */ uint16_t Size; /* Data size in bytes */ uint16_t LoadAddress; /* Load address */ uint16_t ExecAddress; /* Execution address */ char Comment[38]; /* Comment (truncated from CMT's 104) */ }; static const char *QDFileTypes[MAXQDFILETYPES] = { "???", "OBJ", "BTX", "BSD", "BRD", "RB ", "???", "LIB", "???", "???", "SYS", "GR " }; static uint8_t QDArray[QDSIZE]; static char qdFileName[256] = DEFAULTQDFILE; /* ------- Low-level QD block I/O helpers ------- */ static void write_sync(uint16_t *ptr) { QDArray[(*ptr)++] = 0x00; /* BREAK */ QDArray[(*ptr)++] = 0x16; /* SYNC */ QDArray[(*ptr)++] = 0x16; /* SYNC */ QDArray[(*ptr)++] = 0xA5; /* SHARP SYNC */ } static void write_crc(uint16_t *ptr) { QDArray[(*ptr)++] = 'C'; QDArray[(*ptr)++] = 'R'; QDArray[(*ptr)++] = 'C'; } static int save_qd(void) { FILE *f = fopen(qdFileName, "wb"); if (!f) { fprintf(stderr, "ERROR: Cannot write '%s'\n", qdFileName); return 0; } fwrite(QDArray, QDSIZE, 1, f); fclose(f); return 1; } static int load_qd(void) { FILE *f = fopen(qdFileName, "rb"); if (!f) { fprintf(stderr, "ERROR: Cannot open '%s'\n", qdFileName); return 0; } fread(QDArray, QDSIZE, 1, f); fclose(f); return 1; } /* ------- Commands ------- */ /* Format: create an empty QD image with 0 files */ static void QDFormat(void) { uint16_t ptr = 0; printf("Formatting '%s' (%d bytes)\n", qdFileName, QDSIZE); memset(QDArray, 0x00, QDSIZE); /* Disk header: sync + block_count(0) + CRC */ write_sync(&ptr); QDArray[ptr++] = 0x00; /* 0 blocks */ write_crc(&ptr); if (!save_qd()) exit(1); puts("Done."); } /* Dir: list files on the QD */ static void QDDir(void) { uint16_t ptr, blockSize; uint8_t blocks, i; if (!load_qd()) exit(1); /* Disk header: 4 sync bytes + block_count + 3 CRC */ ptr = 4; blocks = QDArray[ptr++]; ptr += 3; /* skip CRC */ printf("Directory of '%s': %d block(s), %d file(s)\n\n", qdFileName, blocks, blocks / 2); for (i = 0; i < blocks; i++) { if (ptr + 7 >= QDSIZE) { fprintf(stderr, "WARNING: Truncated QD image at block %d\n", i); break; } ptr += 4; /* sync: 00 16 16 A5 */ uint8_t blockType = QDArray[ptr++]; blockSize = QDArray[ptr] | (QDArray[ptr + 1] << 8); ptr += 2; if (blockType == 0x00 && blockSize == QDHDRSIZE) { /* File header block */ struct QDHeaderStr *hdr = (struct QDHeaderStr *)&QDArray[ptr]; uint8_t ft = hdr->Attribute; if (ft >= MAXQDFILETYPES) ft = 0; /* Format name: stop at CR or end of field */ char name[18]; memcpy(name, hdr->Name, 17); name[17] = '\0'; for (int j = 0; j < 17; j++) { if (name[j] == '\r' || name[j] == '\0') { name[j] = '\0'; break; } } printf(" %-3s %-17s Size=%-6u Load=0x%04X Exec=0x%04X\n", QDFileTypes[ft], name, hdr->Size, hdr->LoadAddress, hdr->ExecAddress); } else if (blockType == 0x05) { /* Data block — skip silently */ } else { printf(" [block %d: type=0x%02X size=%u]\n", i, blockType, blockSize); } ptr += blockSize; ptr += 3; /* skip CRC */ } printf("\n%u bytes free\n", QDSIZE - ptr); } /* Add: append an MZF file to the QD image */ static void AddFileToQD(const char *mzfFileName) { FILE *mzfFile; uint16_t ptr, fileDataSize; uint8_t blocks, i; uint16_t blockSize; struct CMTHeaderStr cmtHdr; if (!load_qd()) exit(1); /* Read block count and skip to end of existing data */ ptr = 4; blocks = QDArray[ptr]; ptr = 5 + 3; /* past block_count + CRC */ for (i = 0; i < blocks; i++) { ptr += 4; /* sync */ ptr++; /* block type */ blockSize = QDArray[ptr] | (QDArray[ptr + 1] << 8); ptr += 2; ptr += blockSize; ptr += 3; /* CRC */ } /* Open and read MZF file */ mzfFile = fopen(mzfFileName, "rb"); if (!mzfFile) { fprintf(stderr, "ERROR: Cannot open '%s'\n", mzfFileName); exit(1); } fseek(mzfFile, 0, SEEK_END); long mzfTotalSize = ftell(mzfFile); fseek(mzfFile, 0, SEEK_SET); if (mzfTotalSize < CMTHDRSIZE) { fprintf(stderr, "ERROR: '%s' is too small for MZF format (%ld bytes)\n", mzfFileName, mzfTotalSize); fclose(mzfFile); exit(1); } fread(&cmtHdr, CMTHDRSIZE, 1, mzfFile); fileDataSize = (uint16_t)(mzfTotalSize - CMTHDRSIZE); /* Check available space */ uint32_t needed = (4+1+2+QDHDRSIZE+3) + (4+1+2+(uint32_t)fileDataSize+3); if (ptr + needed > QDSIZE) { fprintf(stderr, "ERROR: Not enough space (need %u, have %u)\n", (unsigned)needed, QDSIZE - ptr); fclose(mzfFile); exit(1); } /* ---- Write header block ---- */ write_sync(&ptr); QDArray[ptr++] = 0x00; /* block type: file header */ QDArray[ptr++] = QDHDRSIZE; /* size low */ QDArray[ptr++] = 0x00; /* size high */ struct QDHeaderStr *qdHdr = (struct QDHeaderStr *)&QDArray[ptr]; memset(qdHdr, 0, QDHDRSIZE); /* Convert CMT header to QD header */ qdHdr->Attribute = cmtHdr.Attribute; if (qdHdr->Attribute == 0x05) qdHdr->Attribute = 0x02; /* MZF tape type 05 → QD type 02 (BTX) */ memcpy(qdHdr->Name, cmtHdr.Name, 17); qdHdr->Unknown = 0; /* * Size field handling: * - If MZF Size > 0: use it directly (normal file) * - If MZF Size == 0: use actual file data size (convention for * ROM images where Size=0 means "whole file"). The QD header * MUST have the real size — the QD loader uses it to determine * how many bytes to read. */ if (cmtHdr.Size != 0) qdHdr->Size = cmtHdr.Size; else qdHdr->Size = fileDataSize; qdHdr->LoadAddress = cmtHdr.LoadAddress; qdHdr->ExecAddress = cmtHdr.ExecAddress; memcpy(qdHdr->Comment, cmtHdr.Comment, 38); ptr += QDHDRSIZE; write_crc(&ptr); /* ---- Write data block ---- */ write_sync(&ptr); QDArray[ptr++] = 0x05; /* block type: file data */ QDArray[ptr++] = fileDataSize & 0xFF; QDArray[ptr++] = (fileDataSize >> 8) & 0xFF; fread(&QDArray[ptr], 1, fileDataSize, mzfFile); ptr += fileDataSize; write_crc(&ptr); fclose(mzfFile); /* Update block count (+2: one header block, one data block) */ QDArray[4] = blocks + 2; /* Format name for display */ char name[18]; memcpy(name, cmtHdr.Name, 17); name[17] = '\0'; for (int j = 0; j < 17; j++) { if (name[j] == '\r' || name[j] == '\0') { name[j] = '\0'; break; } } printf(" Added: type=0x%02X name=\"%s\" size=%u load=0x%04X exec=0x%04X\n", qdHdr->Attribute, name, qdHdr->Size, qdHdr->LoadAddress, qdHdr->ExecAddress); printf(" QD: %d blocks, %u bytes free\n", blocks + 2, QDSIZE - ptr); if (!save_qd()) exit(1); } /* ------- Usage and main ------- */ static void usage(void) { printf("Usage:\n"); printf(" MZQDTool format [-o disk.qd] Format empty QD image\n"); printf(" MZQDTool dir [-o disk.qd] List QD directory\n"); printf(" MZQDTool add [-o disk.qd] Add MZF file to QD\n"); printf("\n"); printf("Options:\n"); printf(" -o QD image file (default: %s)\n", DEFAULTQDFILE); printf("\n"); } int main(int argc, char *argv[]) { printf("\nMZQDTool V%s (c) 2002 BKK, 2026 Philip Smart\n\n", VERSION); if (argc < 2) { usage(); return 1; } /* Parse -o option from any position */ for (int i = 1; i < argc - 1; i++) { if (strcmp("-o", argv[i]) == 0) { strncpy(qdFileName, argv[i + 1], sizeof(qdFileName) - 1); qdFileName[sizeof(qdFileName) - 1] = '\0'; /* Remove -o and its argument from argv for command parsing */ for (int j = i; j < argc - 2; j++) argv[j] = argv[j + 2]; argc -= 2; break; } } if (strcmp("format", argv[1]) == 0) { QDFormat(); } else if (strcmp("dir", argv[1]) == 0) { QDDir(); } else if (strcmp("add", argv[1]) == 0) { if (argc < 3) { fprintf(stderr, "ERROR: No MZF file specified\n"); return 2; } AddFileToQD(argv[2]); } else { fprintf(stderr, "ERROR: Unknown command '%s'\n", argv[1]); usage(); return 1; } return 0; }