/* * 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 #if defined(_MSC_VER) # define PACKED(...) \ __pragma(pack(push, 1)) \ __VA_ARGS__ \ __pragma(pack(pop)) #elif defined(__GNUC__) # define PACKED(...) \ __VA_ARGS__ \ __attribute__((packed)) #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 > 1) { 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; }