From e58ce7c1fbbf0b3edc8655d807a2ca52c0689f6b Mon Sep 17 00:00:00 2001 From: Tony Lawrence Date: Thu, 14 Nov 2024 15:22:11 -0500 Subject: [PATCH] IMD2DSK: Offering a new utility to convert IMD files to DSK IMD files are created by the ImageDisk utility in a special format. This tool allows to recreate a pure data file (.DSK) that can be used under SimH. The tool is coded solely based on the open description of the IMD format published by the ImageDisk author, Dave Dunfield. --- README.md | 1 + converters/imd2dsk/Makefile | 20 ++ converters/imd2dsk/imd2dsk.c | 677 +++++++++++++++++++++++++++++++++++ 3 files changed, 698 insertions(+) create mode 100644 converters/imd2dsk/Makefile create mode 100644 converters/imd2dsk/imd2dsk.c diff --git a/README.md b/README.md index da2361f..229e7fd 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ decsys | Convert decimal listing file to a DECtape file dtos8cvt | Convert a PDP-8 DECtape image from OS/8 format to simulator format. gt7cvt | Convert a gt7 magtape dump to a SIMH magtape hpconvert | Convert an HP disc image between SIMH and HPDrive formats +imd2dsk | Convert an ImageDisk (IMD) file to DSK (pure data) indent | Convert simulator sources to 4-column tabs littcvt | Remove density maker from litt format tapes m8376 | Assembles 8 PROM files into a 32bit binary file diff --git a/converters/imd2dsk/Makefile b/converters/imd2dsk/Makefile new file mode 100644 index 0000000..28292d6 --- /dev/null +++ b/converters/imd2dsk/Makefile @@ -0,0 +1,20 @@ +# all of these can be over-ridden on the "make" command line if they don't suit your environment. +TOOL=imd2dsk +CFLAGS=-O2 -DNDEBUG -Wall -Wextra -pedantic +BIN=/usr/local/bin +INSTALL=install +CC=gcc + +$(TOOL): $(TOOL).c + $(CC) $(CFLAGS) $(LDFLAGS) -o $(TOOL) $(TOOL).c $(LDLIBS) + +.PHONY: clean install uninstall + +clean: + rm -f $(TOOL) + +install: $(TOOL) + $(INSTALL) -p -m u=rx,g=rx,o=rx $(TOOL) $(BIN) + +uninstall: + rm -f $(BIN)/$(TOOL) diff --git a/converters/imd2dsk/imd2dsk.c b/converters/imd2dsk/imd2dsk.c new file mode 100644 index 0000000..76624d3 --- /dev/null +++ b/converters/imd2dsk/imd2dsk.c @@ -0,0 +1,677 @@ +/* + * IMD -> DSK (pure data) file converter + * + * Copyright (c) 2024 Tony Lawrence + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the + * following conditions are met: + * + * 1. Redistributions of source code must retain the above + * copyright notice, this list of conditions and the following + * disclaimer. + * + * 2. Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names + * of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +# define PACKED(...) \ + __pragma(pack(push, 1)) \ + __VA_ARGS__ \ + __pragma(pack(pop)) +#else +# if defined(__GNUC__) +# define PACKED(...) \ + __VA_ARGS__ \ + __attribute__((packed)) +# endif +#endif + + +typedef unsigned char uint1; +typedef signed char int1; +typedef unsigned short uint2; +typedef short int2; +typedef uint32_t uint4; +typedef int32_t int4; + + +/* IMD track header */ +PACKED(struct S_IMDTrkHdr { + uint1 mode; /* Track write mode (check only) */ + uint1 cyl; /* Cylinder # */ + uint1 head; /* Head(side) 0|1 and map flags */ + uint1 nsect; /* Number of sectors to follow */ + uint1 ssize; /* Sector size or 0xFF for size tbl */ +}); + +#define MODE_MAX 5 /* Maximal valid track write mode */ + +#define SECTOR_CYL_MAP 0x80 /* Cylinder map for each sector */ +#define SECTOR_HEAD_MAP 0x40 /* Head map for each sector */ + +#define SECTOR_SIZE_TBL 0xFF /* Sector size table for each sector*/ +#define SECTOR_SIZE_MAX 8192 /* Maximal sector size supported */ + +#define IMD_HEADER_END '\x1A' /* Ctrl-Z (Text file EOF in MS-DOS) */ + + +static int verbose = 0; + +#define VERBOSE_SUMMARY 1 +#define VERBOSE_HEADER 2 +#define VERBOSE_TRACK 3 +#define VERBOSE_SECTOR 4 + + +/* Check IMD file header and return file pos where it ends */ +static long x_skip_header(FILE* fp, const char* file) +{ + int major, minor, ch; + struct tm tm, tmp; + + memset(&tm, 0, sizeof(tm)); + if (fscanf(fp, "IMD %d.%d: %d/%d/%d %d:%d:%d", + &major, &minor, + &tm.tm_mday, &tm.tm_mon, &tm.tm_year, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 8) { + goto out; + } + if (major <= 0 || minor < 0) + goto out; + if (tm.tm_mday < 1 || tm.tm_mday > 31 || + tm.tm_mon < 1 || tm.tm_mon > 12 || + tm.tm_year < 1900 || + tm.tm_hour < 0 || tm.tm_hour > 23 || + tm.tm_min < 0 || tm.tm_min > 59 || + tm.tm_sec < 0 || tm.tm_sec > 60) { + goto out; + } + tm.tm_mon--; + tm.tm_year -= 1900; + memcpy(&tmp, &tm, sizeof(tmp)); + if (mktime(&tmp) == (time_t)(-1L)) + goto out; + if (tm.tm_mday != tmp.tm_mday || + tm.tm_mon != tmp.tm_mon || + tm.tm_year != tmp.tm_year) { + /* NB: No check for time because + * the ranges are already okay yet + * DST can screw up the hours... */ + goto out; + } + + ch = fgetc(fp); + if (ch != IMD_HEADER_END) { + int/*bool*/ space = 1/*true*/; + if (!isspace(ch)) + goto out; + if (verbose >= VERBOSE_HEADER) { + char timebuf[80]; + strftime(timebuf, sizeof(timebuf), "%d/%m/%Y %T", &tm); + printf("IMD %d.%d: %s\n", major, minor, timebuf); + } + for (;;) { + ch = fgetc(fp); + if (ch == IMD_HEADER_END) + break; + if (isspace(ch)) { + if (space) + continue; + } else { + if (!isprint(ch)) + goto out; + space = 0/*false*/; + } + if (verbose >= VERBOSE_HEADER) + putchar(ch); + } + if (!space && verbose >= VERBOSE_HEADER) + putchar('\n'); + } + + return ftell(fp); + +out: + fprintf(stderr, "%s: Invalid IMD file header\n", file); + return 0; +} + + +inline +static int/*bool*/ x_check_mode(uint1 mode) +{ + return mode > MODE_MAX ? 0/*false*/ : 1/*true*/; +} + + +inline +static uint2 x_sector_size(uint1 ssize) +{ + return ssize > 6 ? SECTOR_SIZE_MAX + 1 : 1 << (7 + ssize); +} + + +static int/*bool*/ x_skip_sector(FILE* fp, uint2 ssize) +{ + int ch = fgetc(fp); + if (ch == EOF) + return 0/*false*/; + if (ch < 0 || ch > 8) + return 0/*false*/; + if (ch == 0) + return 1/*true*/; + return fseek(fp, ch & 1 ? ssize : 1, SEEK_CUR) == 0 ? 1/*T*/ : 0/*F*/; +} + + +/* Disk parameters */ +static int cyls = 0; +static int heads = 0; +static int sectors = 0; +static int sector_size = 0; + +/* Sector numbering */ +static int max_sect = 0; +static int zero_sect = 0; +static int one_based = 1; + + +/* Scan the file, set up disk params and return the number of tracks found */ +static int x_scan_tracks(FILE* fp, const char* file) +{ + const char* problem = 0; + uint1 smap[256]; + char buf[80]; + int track; + + for (track = 0; ; ++track) { + struct S_IMDTrkHdr hdr; + uint2* stable; + uint2 ssize; + long skip; + int n; + + if (fread(&hdr, sizeof(hdr), 1, fp) != 1) { + if (feof(fp) && track) + break; + problem = "File read error"; + goto out; + } + if (!x_check_mode(hdr.mode)) { + sprintf(buf, "Unrecognized track write mode %u", hdr.mode); + problem = buf; + goto out; + } + ssize = hdr.ssize; + if (ssize != SECTOR_SIZE_TBL) { + ssize = x_sector_size(hdr.ssize); + assert(ssize); + if (ssize & (ssize - 1)) { + sprintf(buf, "Invalid sector size %hu", ssize); + problem = buf; + goto out; + } + assert(ssize <= SECTOR_SIZE_MAX); + if (!sector_size) + sector_size = ssize; + else if (sector_size != ssize) { + sprintf(buf, "Sector size change %d -> %hu", sector_size, ssize); + problem = buf; + goto out; + } + } + n = hdr.head & ~(SECTOR_CYL_MAP | SECTOR_HEAD_MAP); + if (n & ~1) { + sprintf(buf, "Invalid head number %d", n); + problem = buf; + goto out; + } + if (heads <= n) + heads++; + if (!hdr.nsect) + continue; + if (sectors < hdr.nsect) + sectors = hdr.nsect; + if (fread(smap, hdr.nsect, 1, fp) != 1) { + problem = "Cannot read sector map"; + goto out; + } + for (n = 0; n < hdr.nsect; ++n) { + if (!smap[n]) { + ++zero_sect; + continue; + } + if (max_sect < smap[n]) + max_sect = smap[n]; + } + skip = 0; + if (hdr.head & SECTOR_CYL_MAP) + skip += hdr.nsect; + if (hdr.head & SECTOR_HEAD_MAP) + skip += hdr.nsect; + if (skip && fseek(fp, skip, SEEK_CUR) != 0) { + problem = "File positioning error"; + goto out; + } + if (ssize == SECTOR_SIZE_TBL) { + if (!(stable = (uint2*) malloc(sizeof(*stable) * hdr.nsect))) { + problem = "Cannot allocate for sector size table"; + goto out; + } + if (fread(stable, sizeof(*stable), hdr.nsect, fp) != hdr.nsect) { + problem = "Cannot read sector size table"; + goto out; + } + } else + stable = 0; + for (n = 0; n < hdr.nsect; ++n) { + if (stable) { + ssize = stable[n]; + if (!ssize || (ssize & (ssize - 1)) || ssize > SECTOR_SIZE_MAX) { + sprintf(buf, "Invalid sector size %hu in sector %d", + ssize, n + 1); + problem = buf; + break; + } + if (!sector_size) + sector_size = ssize; + else if (sector_size != ssize) { + sprintf(buf, "Sector size change %d -> %hu in sector %d", + sector_size, ssize, n + 1); + problem = buf; + break; + } + } else + assert(ssize && !(ssize & (ssize - 1)) && ssize <= SECTOR_SIZE_MAX); + if (!x_skip_sector(fp, ssize)) { + sprintf(buf, "Error skipping sector %d (size %hu)", n + 1, ssize); + problem = buf; + break; + } + } + if (stable) + free(stable); + if (n < hdr.nsect) { + assert(problem); + goto out; + } + } + + return track; + +out: + assert(problem); + fprintf(stderr, "%s (scanning track data %d): %s\n", file, track, problem); + return 0/*error*/; +} + + +/* Summary */ +static int n_restored = 0; +static int n_compressed = 0; + +/* Storage map */ +static uint1* map = 0; + + +static int/*bool*/ x_extract_sector(int C, int H, int S, int track, + FILE* in, const char* infile, + FILE* out, const char* outfile) +{ + static uint1 sector[SECTOR_SIZE_MAX]; + int/*bool*/ duplicate = 0/*false*/; + const char* problem = 0; + const char* file = 0; + int ch; + + /* Disk Address */ + int da = (C * heads + H) * sectors + S - one_based; + assert(0 <= da && da < cyls * heads * sectors); + + if (map[da >> 3] & (1 << (da & 7))) { + fprintf(stderr, "%s: WARNING -- Duplicate sector %d/%d/%d in track data %d\n", + infile, C, H, S, track); + duplicate = 1/*true*/; + } else + map[da >> 3] |= 1 << (da & 7); + + ch = fgetc(in); + if (ch == EOF) { + problem = "File read error"; + file = infile; + goto out; + } + + switch (ch) { + case 0: + problem = "Sector data unavailable"; + break; + case 1: case 2: + /* normal sector data */ + assert(!problem); + break; + case 3: case 4: + problem = "Deleted data"; + break; + case 5: case 6: + problem = "Sector data with error"; + break; + case 7: case 8: + problem = "Deleted data with error"; + break; + default: + /* NB: this should never happen per x_skip_sector() */ + abort(); + } + if (problem) + fprintf(stderr, "%s: WARNING -- %d/%d/%d: %s\n", infile, C, H, S, problem); + + assert(0 < sector_size && sector_size <= (int) sizeof(sector)); + if (ch & 1) { + if (fread(sector, sector_size, 1, in) != 1) { + problem = "Sector data read error"; + file = infile; + goto out; + } + } else { + if (ch) { + ch = fgetc(in); + if (ch == EOF) { + problem = "Compressed sector data read error"; + file = infile; + goto out; + } + } /* else: technically it's still a "compressed" sector */ + memset(sector, ch, sector_size); + if (!duplicate) + ++n_compressed; + } + + da *= sector_size; + if (fseek(out, da, SEEK_SET) != 0) { + problem = "File positioning error"; + file = outfile; + goto out; + } + if (fwrite(sector, sector_size, 1, out) != 1) { + problem = "File write error"; + file = outfile; + goto out; + } + + if (!duplicate) + ++n_restored; + return 1/*true*/; + +out: + assert(file && problem); + fprintf(stderr, "%s (extracting track data %d for %d/%d/%d): %s\n", + file, track, C, H, S, problem); + return 0/*false*/; +} + + +static int/*bool*/ x_extract_track(int track, int/*bool*/ no_map, + FILE* in, const char* infile, + FILE* out, const char* outfile) +{ + static uint1 cmap[256]; + static uint1 hmap[256]; + static uint1 smap[256]; + const char* problem = 0; + struct S_IMDTrkHdr hdr; + char buf[80]; + long skip; + int n; + + if (verbose >= VERBOSE_TRACK) + fprintf(stderr, "Track data %d\n", track); + + if (fread(&hdr, sizeof(hdr), 1, in) != 1) { + problem = "File read error"; + goto out; + } + assert(x_check_mode(hdr.mode)); + if (!hdr.nsect) + return 1/*true*/; + + if (fread(smap, hdr.nsect, 1, in) != 1) { + problem = "Cannot read sector map"; + goto out; + } + skip = 0; + if (no_map) { + if (hdr.head & SECTOR_CYL_MAP) + skip += hdr.nsect; + if (hdr.head & SECTOR_HEAD_MAP) + skip += hdr.nsect; + hdr.head &= ~(SECTOR_CYL_MAP | SECTOR_HEAD_MAP); + } else { + if ((hdr.head & SECTOR_CYL_MAP) + && fread(cmap, hdr.nsect, 1, in) != 1) { + problem = "Cannot read cylinder map"; + goto out; + } + if ((hdr.head & SECTOR_HEAD_MAP) + && fread(hmap, hdr.nsect, 1, in) != 1) { + problem = "Cannot read head map"; + goto out; + } + } + if (hdr.ssize == SECTOR_SIZE_TBL) + skip += sizeof(uint2) * hdr.nsect; + if (skip && fseek(in, skip, SEEK_CUR) != 0) { + problem = "File positioning error"; + goto out; + } + + for (n = 0; n < hdr.nsect; ++n) { + int C = hdr.head & SECTOR_CYL_MAP ? cmap[n] : hdr.cyl; + int H = hdr.head & SECTOR_HEAD_MAP ? hmap[n] : hdr.head & 0xF; + int S = smap[n]; + if (verbose >= VERBOSE_SECTOR) + fprintf(stderr, "Extracting: %d/%d/%d\n", C, H, S); + if (C < 0 || C >= cyls) { + sprintf(buf, "Cylinder %d out of range [0..%d]", C, cyls - 1); + problem = buf; + goto out; + } + if (H < 0 || H >= heads) { + sprintf(buf, "Head %d out of range [0..%d]", H, heads - 1); + problem = buf; + goto out; + } + if (S < one_based || S >= sectors + one_based) { + sprintf(buf, "Sector %d out of range [%d..%d]", S, + one_based, sectors - !one_based); + problem = buf; + goto out; + } + if (!x_extract_sector(C, H, S, track, in, infile, out, outfile)) + return 0/*false*/; + } + + return 1/*true*/; + +out: + assert(problem); + fprintf(stderr, "%s (extracting track data %d): %s\n", infile, track, problem); + return 0/*false*/; +} + + +#ifdef __GNUC__ +__attribute__((noreturn)) +#endif +static void usage(const char* prog) +{ + fprintf(stderr, "%s [-i] [-v...] infile outfile\n\n", prog); + fprintf(stderr, "-i = Ignore cylinder / head maps\n" + "-v = Increase verbosity with each occurrence\n"); + exit(2); +} + + +#if defined(__CYGWIN__) +# define _stricmp strcasecmp +#elif !defined(_MSC_VER) +# define _stricmp strcmp +#else +# define strcmp _strcmp +#endif + + +int main(int argc, char* argv[]) +{ + /* whether to ignore cyl / head maps */ + int/*bool*/ no_map = 0/*false*/; + const char* infile; + const char* outfile; + int p, q, tracks; + FILE* in; + FILE* out; + long pos; + + p = 1; + if (argc > 3) { + do { + if (strcmp(argv[p], "-v") == 0) { + ++verbose; + continue; + } + if (strcmp(argv[p], "-i") != 0) + break; + if (no_map) + usage(argv[0]); + no_map = 1/*ignore map*/; + } while (argv[++p]); + } + if (p < argc && strcmp(argv[p], "--") == 0) + ++p; + if (argc < 3 + || !(infile = argv[p++]) || !(outfile = argv[p++]) || argv[p] + || _stricmp(infile, outfile) == 0) { + usage(argv[0]); + } + + if (!(in = fopen(infile, "rb"))) { + perror(infile); + return EXIT_FAILURE; + } + + if (!(pos = x_skip_header(in, infile))) + return EXIT_FAILURE; + if (pos == -1L) { + fprintf(stderr, "%s: File positioning error", infile); + return EXIT_FAILURE; + } + + if (!(tracks = x_scan_tracks(in, infile))) + return EXIT_FAILURE; + assert(heads <= 2 && sectors <= 255 && sector_size <= SECTOR_SIZE_MAX); + + if (heads <= 0 || sectors <= 0 || sector_size <= 0 + || (cyls = (tracks + heads - 1) / heads) <= 0 || 255 < cyls) { + fprintf(stderr, "%s: Failed to determine disk geometry, sorry\n", infile); + return EXIT_FAILURE; + } + + if (zero_sect > (tracks >> 2) && max_sect < sectors) + one_based = 0; + if (verbose >= VERBOSE_SUMMARY) { + fprintf(stderr, "%s: CHS = %d/%d/%d; Sector size = %d%s\n", + infile, cyls, heads, sectors, sector_size, + one_based ? "" : "; 0-based sector numbering"); + } + + if (fseek(in, pos, SEEK_SET) != 0) { + perror(infile); + return EXIT_FAILURE; + } + + q = cyls * heads * sectors; + assert(0 < q && q <= 255 * 2 * 255 /*130050*/); + if (!(map = (uint1*) calloc((q + 7) / 8, sizeof(*map)))) { + perror(outfile); + return EXIT_FAILURE; + } + + if (!(out = fopen(outfile, "wb"))) { + perror(outfile); + return EXIT_FAILURE; + } + + for (p = 0; p < tracks; ++p) { + if (!x_extract_track(p, no_map, in, infile, out, outfile)) + return EXIT_FAILURE; + } + + for (p = 0; p < q; p += 8) { + int k, n = p >> 3; + if (map[n] == (1 << 8) - 1) + continue; + for (k = 0; k < 8; ++k) { + int C, H, S = p | k; + if (S >= q) + break; + if (map[n] & (1 << k)) + continue; + H = S / sectors; + S %= sectors; + C = H / heads; + H %= heads; + fprintf(stderr, "%s: WARNING -- Sector not stored: %d/%d/%d\n", + outfile, C, H, S + one_based); + } + } + + free(map); + + if (fclose(out) != 0) { + fprintf(stderr, "%s: Closing error\n", outfile); + return EXIT_FAILURE; + } + if (verbose >= VERBOSE_SUMMARY) { + printf("%s: Total sectors restored: %d", outfile, n_restored); + if (n_compressed) + printf(", of those compressed: %d\n", n_compressed); + else + putchar('\n'); + } + fclose(in); + return EXIT_SUCCESS; +}