367 lines
11 KiB
C
Executable File
367 lines
11 KiB
C
Executable File
/* =========================================================================
|
|
* 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 <file.mzf> [-o disk.qd]
|
|
* ========================================================================= */
|
|
|
|
#define VERSION "1.00"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
|
|
#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 <file.mzf> [-o disk.qd] Add MZF file to QD\n");
|
|
printf("\n");
|
|
printf("Options:\n");
|
|
printf(" -o <filename> 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;
|
|
}
|