diff --git a/README.md b/README.md index 5fd7e09..047396a 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 lbn2pbn | Logical-to-physical converter for single-sided floppy disk images littcvt | Remove density maker from litt format tapes 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; +}