From 357940ff9bc99a11e3790c31cc21ec7043ab3785 Mon Sep 17 00:00:00 2001 From: Wes Biggs Date: Mon, 2 Nov 2026 16:02:11 +0000 Subject: [PATCH 1/2] firmware: add the D64/D71/D81 engine that GEOS, the block device and VDrive will share The GEOS installer and the TR block device each carry their own image geometry, BAM and directory code, and VDrive brings a third, more complete engine (40-track D64s, error bytes, BAM and directory growth for all three kinds, tested against c1541). Bring VDrive's engine in under Source/Teensy/DiskImage so the other two can move onto it. Nothing uses it yet. The copy differs from teensyrom-vdrive's d64/ only in what the firmware needs: SPDX lines, the placement macros moving into d64.h, and its three constant tables marked D64_FLASH so they land in PROGMEM rather than DTCM. DiskImage.h includes the code once for the single translation unit the firmware and the host harnesses build. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X5t9wWXadFkQgKaCnjreF7 --- Source/Teensy/DiskImage/DiskImage.h | 17 + Source/Teensy/DiskImage/d64.c | 1169 ++++++++++++++++++++++ Source/Teensy/DiskImage/d64.h | 215 ++++ Source/Teensy/tests/disk-image.test.js | 65 ++ Source/Teensy/tests/disk_image_harness.c | 64 ++ docs/Architecture/Disk-Images.md | 34 + docs/Architecture/Overview.md | 1 + 7 files changed, 1565 insertions(+) create mode 100644 Source/Teensy/DiskImage/DiskImage.h create mode 100644 Source/Teensy/DiskImage/d64.c create mode 100644 Source/Teensy/DiskImage/d64.h create mode 100644 Source/Teensy/tests/disk-image.test.js create mode 100644 Source/Teensy/tests/disk_image_harness.c create mode 100644 docs/Architecture/Disk-Images.md diff --git a/Source/Teensy/DiskImage/DiskImage.h b/Source/Teensy/DiskImage/DiskImage.h new file mode 100644 index 00000000..fca1c7f8 --- /dev/null +++ b/Source/Teensy/DiskImage/DiskImage.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// The D64/D71/D81 engine that the GEOS support, the TR block device and VDrive share (docs/Architecture/Disk-Images.md). +// The firmware builds every .ino and the headers they include as one translation unit, and so does each host +// harness, so the code comes in once, here, behind the guard. +#pragma once +#ifdef ARDUINO +#include +#ifndef VD_COLD +#define VD_COLD FLASHMEM +#endif +#ifndef D64_FLASH +#define D64_FLASH PROGMEM +#endif +#endif + +#include "d64.h" +#include "d64.c" diff --git a/Source/Teensy/DiskImage/d64.c b/Source/Teensy/DiskImage/d64.c new file mode 100644 index 00000000..870eb073 --- /dev/null +++ b/Source/Teensy/DiskImage/d64.c @@ -0,0 +1,1169 @@ +/* SPDX-License-Identifier: MIT */ +#include "d64.h" +#include + +/* sectors per track of a 1541 (also each side of a 1571) */ +VD_COLD static int spt35(uint8_t t) +{ + if (t >= 1 && t <= 17) return 21; + if (t >= 18 && t <= 24) return 19; + if (t >= 25 && t <= 30) return 18; + if (t >= 31 && t <= 40) return 17; + return 0; +} + +VD_COLD static int spt(const d64_t *d, uint8_t t) +{ + if (d->fmt == D64_FMT_D81) return (t >= 1 && t <= 80) ? 40 : 0; + if (d->fmt == D64_FMT_D71) return t > 35 ? (t <= 70 ? spt35((uint8_t)(t - 35)) : 0) : spt35(t); + return spt35(t); +} +VD_COLD int d64_sectors_in_track(const d64_t *d, uint8_t t) { return spt(d, t); } + +VD_COLD long d64_sector_offset(const d64_t *d, uint8_t t, uint8_t s) +{ + if (t < 1 || t > d->tracks || s >= spt(d, t)) return -1; + long off = 0; + if (d->fmt == D64_FMT_D81) off = (long)(t - 1) * 40; + else for (uint8_t i = 1; i < t; i++) off += spt(d, i); + return (off + s) * D64_SECTOR; +} + +/* file size -> format and track count (images may carry one error byte per sector after the data) */ +VD_COLD static int classify(size_t len, uint8_t *tracks, uint8_t *fmt) +{ + if (len >= 819200) { *fmt = D64_FMT_D81; *tracks = 80; } + else if (len >= 349696) { *fmt = D64_FMT_D71; *tracks = 70; } + else if (len >= 196608) { *fmt = D64_FMT_D64; *tracks = 40; } + else if (len >= 174848) { *fmt = D64_FMT_D64; *tracks = 35; } + else return -1; + return 0; +} + +VD_COLD static void geometry(d64_t *d, uint8_t tracks) +{ + d->fmt = tracks == 80 ? D64_FMT_D81 : tracks == 70 ? D64_FMT_D71 : D64_FMT_D64; + d->tracks = tracks == 80 || tracks == 70 || tracks == 40 ? tracks : 35; +} + +VD_COLD static int mem_write(void *ctx, uint8_t t, uint8_t s, const uint8_t *buf) +{ + d64_mem_t *m = (d64_mem_t *)ctx; + d64_t tmp; memset(&tmp, 0, sizeof tmp); + if (classify(m->len, &tmp.tracks, &tmp.fmt)) return -1; + long off = d64_sector_offset(&tmp, t, s); + if (off < 0 || (size_t)off + D64_SECTOR > m->len) return -1; + memcpy(m->data + off, buf, D64_SECTOR); + return 0; +} + +VD_COLD static int mem_read(void *ctx, uint8_t t, uint8_t s, uint8_t *buf) +{ + d64_mem_t *m = (d64_mem_t *)ctx; + d64_t tmp; memset(&tmp, 0, sizeof tmp); + if (classify(m->len, &tmp.tracks, &tmp.fmt)) return -1; + long off = d64_sector_offset(&tmp, t, s); + if (off < 0 || (size_t)off + D64_SECTOR > m->len) return -1; + memcpy(buf, m->data + off, D64_SECTOR); + return 0; +} +VD_COLD void d64_mem_init(d64_mem_t *m, uint8_t *data, size_t len) { m->data = data; m->len = len; } +VD_COLD int d64_open(d64_t *d, d64_read_fn rd, void *ctx, uint8_t tracks) +{ + d->read = rd; d->write = NULL; d->ctx = ctx; d->ro = 0; + d->errtab = NULL; d->bad = d->bad_t = d->bad_s = 0; + geometry(d, tracks); + return 0; +} +VD_COLD int d64_open_size(d64_t *d, d64_read_fn rd, void *ctx, size_t size) +{ + uint8_t tracks, fmt; + if (classify(size, &tracks, &fmt)) return -1; + return d64_open(d, rd, ctx, tracks); +} +VD_COLD int d64_sectors(const d64_t *d) +{ + int n = 0; + for (uint8_t t = 1; t <= d->tracks; t++) n += spt(d, t); + return n; +} +VD_COLD long d64_errtab_offset(const d64_t *d, size_t size) +{ + size_t n = (size_t)d64_sectors(d); + return size == n * (D64_SECTOR + 1) ? (long)(n * D64_SECTOR) : -1; +} +VD_COLD int d64_errcode(uint8_t e) +{ + static const uint8_t code[16] D64_FLASH = { 0, 0, 20, 21, 22, 23, 24, 0, 0, 27, 0, 29, 0, 0, 0, 74 }; + return e < 16 ? code[e] : 0; +} +VD_COLD int d64_open_mem(d64_t *d, d64_mem_t *m) +{ + int r = d64_open_size(d, mem_read, m, m->len); + if (r) return r; + d->write = mem_write; + long eo = d64_errtab_offset(d, m->len); + if (eo >= 0) d->errtab = m->data + eo; + return 0; +} + +VD_COLD static int rd(d64_t *d, uint8_t t, uint8_t s, uint8_t *b) +{ + long off = d64_sector_offset(d, t, s); + if (off < 0) return D64_E_IO; + if (d->read(d->ctx, t, s, b)) return D64_E_IO; + int e = d->errtab ? d64_errcode(d->errtab[off / D64_SECTOR]) : 0; + if (!e) return 0; + if (!d->bad) { d->bad = (uint8_t)e; d->bad_t = t; d->bad_s = s; } + return D64_E_BADSEC; +} +VD_COLD static int wr(d64_t *d, uint8_t t, uint8_t s, const uint8_t *b) +{ + if (!d->write || d->ro) return D64_E_RO; + if (d64_sector_offset(d, t, s) < 0) return D64_E_IO; + return d->write(d->ctx, t, s, b) ? D64_E_IO : 0; +} + +/* ---- layout of the three formats ---- + * d64: header + BAM 18/0 (name $90, id $A2, DOS $A5), directory 18/1.., 4 bytes per track at $04 (count + 3 bitmap bytes) + * d71: as d64 for tracks 1-35, flag $80 at 18/0 byte 3; side 2 counts at 18/0 $DD.., bitmaps (3 bytes per track) in 53/0 + * d81: header 40/0 (name $04, id $16, DOS $19), BAM 40/1 (tracks 1-40) and 40/2 (41-80), 6 bytes per track from $10 + * (count + 5 bitmap bytes), directory 40/3.. + * A set bitmap bit = free sector; sector s is bit (s % 8) of byte (s / 8). */ +/* a file can't have more sectors than the disk: longer chains are loops */ +VD_COLD static uint16_t chain_limit(const d64_t *d) { return d->fmt == D64_FMT_D81 ? 3300 : d->fmt == D64_FMT_D71 ? 1400 : 800; } +VD_COLD static uint8_t dir_track(const d64_t *d) { return d->fmt == D64_FMT_D81 ? 40 : 18; } +VD_COLD static uint8_t dir_sector0(const d64_t *d) { return d->fmt == D64_FMT_D81 ? 3 : 1; } /* first directory sector */ +VD_COLD static uint8_t bam_tracks(const d64_t *d) { return d->fmt == D64_FMT_D81 ? 80 : d->fmt == D64_FMT_D71 ? 70 : 35; } +VD_COLD static uint8_t bam_reserved(const d64_t *d, uint8_t t) /* tracks never given to files: directory (and the d71 side-2 BAM) */ +{ + return t == dir_track(d) || (d->fmt == D64_FMT_D71 && t == 53); +} + +typedef struct { uint8_t ct, cs, co, bt, bs, bo, nb; } bamloc_t; /* count byte at (ct,cs)+co, bitmap at (bt,bs)+bo, nb bytes */ + +VD_COLD static void bam_loc(const d64_t *d, uint8_t t, bamloc_t *b) +{ + if (d->fmt == D64_FMT_D81) { + uint8_t u = t > 40 ? (uint8_t)(t - 40) : t; + b->ct = b->bt = 40; b->cs = b->bs = t > 40 ? 2 : 1; + b->co = (uint8_t)(16 + 6 * (u - 1)); b->bo = (uint8_t)(b->co + 1); b->nb = 5; + } else if (d->fmt == D64_FMT_D71 && t > 35) { + b->ct = 18; b->cs = 0; b->co = (uint8_t)(0xDD + (t - 36)); + b->bt = 53; b->bs = 0; b->bo = (uint8_t)(3 * (t - 36)); b->nb = 3; + } else { + b->ct = b->bt = 18; b->cs = b->bs = 0; + b->co = (uint8_t)(4 * t); b->bo = (uint8_t)(4 * t + 1); b->nb = 3; + } +} + +/* Read the BAM sector(s) for track t; *cnt / *bits point into bufA / bufB. */ +VD_COLD static int bam_load(d64_t *d, uint8_t t, bamloc_t *L, uint8_t *bufA, uint8_t *bufB, uint8_t **cnt, uint8_t **bits) +{ + bam_loc(d, t, L); + int r = rd(d, L->bt, L->bs, bufA); + if (r) return r; + *bits = bufA + L->bo; + if (L->ct == L->bt && L->cs == L->bs) { *cnt = bufA + L->co; return 0; } + if ((r = rd(d, L->ct, L->cs, bufB))) return r; + *cnt = bufB + L->co; + return 0; +} + +VD_COLD static int bam_store(d64_t *d, const bamloc_t *L, const uint8_t *bufA, const uint8_t *bufB) +{ + int r = wr(d, L->bt, L->bs, bufA); + if (r) return r; + if (L->ct != L->bt || L->cs != L->bs) r = wr(d, L->ct, L->cs, bufB); + return r; +} + +VD_COLD static void strip(char *dst, const uint8_t *src, int n) +{ + int len = n; + while (len > 0 && src[len - 1] == 0xA0) len--; + memcpy(dst, src, len); + dst[len] = 0; +} + +VD_COLD int d64_header(d64_t *d, char name[17], char id[3], char dos[3]) +{ + uint8_t b[D64_SECTOR]; + int n = d->fmt == D64_FMT_D81 ? 0x04 : 0x90, i = d->fmt == D64_FMT_D81 ? 0x16 : 0xA2, o = d->fmt == D64_FMT_D81 ? 0x19 : 0xA5; + if (rd(d, dir_track(d), 0, b)) return -1; + strip(name, b + n, 16); + id[0] = b[i]; id[1] = b[i + 1]; id[2] = 0; + dos[0] = b[o]; dos[1] = b[o + 1]; dos[2] = 0; + return 0; +} + +VD_COLD int d64_blocks_free(d64_t *d) +{ + uint8_t b[D64_SECTOR], held_t = 0, held_s = 0xFF; + int free_ = 0; + for (uint8_t t = 1; t <= bam_tracks(d); t++) { + bamloc_t L; + bam_loc(d, t, &L); + if (L.ct != held_t || L.cs != held_s) { if (rd(d, L.ct, L.cs, b)) return -1; held_t = L.ct; held_s = L.cs; } + if (!bam_reserved(d, t)) free_ += b[L.co]; + } + return free_; +} + +VD_COLD int d64_dir_first(d64_t *d, d64_dir_t *it) +{ + uint8_t b[D64_SECTOR]; + if (rd(d, dir_track(d), 0, b)) return -1; + it->track = b[0] ? b[0] : dir_track(d); + it->sector = b[0] ? b[1] : dir_sector0(d); + it->slot = 8; /* force load on first next() */ + it->guard = 0; + return 0; +} + +VD_COLD int d64_dir_next(d64_t *d, d64_dir_t *it, d64_entry_t *e) +{ + for (;;) { + if (it->slot >= 8) { + if (it->track == 0) return 0; + if (++it->guard > 64) return -2; /* loop in chain */ + if (rd(d, it->track, it->sector, it->buf)) return -1; + it->cur_t = it->track; it->cur_s = it->sector; + it->track = it->buf[0]; it->sector = it->buf[1]; /* next sector in chain */ + it->slot = 0; + } + const uint8_t *p = it->buf + it->slot * 32; + it->slot++; + if (p[2] == 0) continue; /* scratched / empty */ + e->type = p[2]; + e->track = p[3]; e->sector = p[4]; + e->blocks = p[30] | (p[31] << 8); + strip(e->name, p + 5, 16); + e->dir_t = it->cur_t; e->dir_s = it->cur_s; e->dir_slot = (uint8_t)(it->slot - 1); + e->ss_t = p[21]; e->ss_s = p[22]; e->reclen = p[23]; + return 1; + } +} + +VD_COLD int d64_match(const char *name, const char *pat) +{ + for (;; name++, pat++) { + if (*pat == '*') return 1; + if (*pat == 0) return *name == 0; + if (*name == 0) return 0; + if (*pat != '?' && *pat != *name) return 0; + } +} + +VD_COLD int d64_find(d64_t *d, const char *pat, d64_entry_t *e) +{ + d64_dir_t it; + int r = d64_dir_first(d, &it); + if (r) return r; + while ((r = d64_dir_next(d, &it, e)) > 0) + if ((e->type & 7) != 0 || (e->type & 0x80)) if (d64_match(e->name, pat)) return 1; + return r; +} + +VD_COLD int d64_file_open(d64_t *d, const d64_entry_t *e, d64_file_t *f) +{ + (void)d; + f->next_t = e->track; f->next_s = e->sector; + f->loaded = 0; f->pos = f->end = 0; f->guard = 0; + return 0; +} + +VD_COLD int d64_file_eof(const d64_file_t *f) +{ + return f->pos >= f->end && f->next_t == 0; +} + +VD_COLD int d64_file_read(d64_t *d, d64_file_t *f, uint8_t *out, size_t n) +{ + size_t got = 0; + while (got < n) { + if (f->pos >= f->end) { + if (f->next_t == 0) break; /* EOF (or empty file) */ + if (++f->guard > chain_limit(d)) return -2; /* chain loop */ + if (rd(d, f->next_t, f->next_s, f->buf)) return -1; + uint8_t lt = f->buf[0], ls = f->buf[1]; + f->pos = 2; + f->end = lt ? 256 : (uint16_t)(ls < 1 ? 2 : ls + 1); /* last sector: ls = index of last used byte */ + f->next_t = lt; f->next_s = ls; + f->loaded = 1; + continue; + } + size_t c = (size_t)(f->end - f->pos); + if (c > n - got) c = n - got; + memcpy(out + got, f->buf + f->pos, c); + f->pos = (uint16_t)(f->pos + c); got += c; + } + return (int)got; +} + +/* ---- write support ------------------------------------------------------ */ + +/* allocation order: nearest the directory first, one side then the other; 0 = exhausted */ +VD_COLD static int next_data_track(const d64_t *d, uint8_t t) +{ + if (d->fmt == D64_FMT_D81) { + if (t == 0) return 39; + if (t >= 2 && t <= 39) return t - 1; + if (t == 1) return 41; + if (t >= 41 && t <= 79) return t + 1; + return 0; + } + if (t == 0) return 17; + if (t >= 2 && t <= 17) return t - 1; + if (t == 1) return 19; + if (t >= 19 && t <= 34) return t + 1; + if (d->fmt == D64_FMT_D71) { + if (t == 35) return 52; + if (t >= 37 && t <= 52) return t - 1; + if (t == 36) return 54; + if (t >= 54 && t <= 69) return t + 1; + } + return 0; +} + +/* Allocate one sector. In: *t,*s = hint (t==0: none). dirmode: the directory track only, never sector 0. */ +VD_COLD static int alloc_sector(d64_t *d, uint8_t *t, uint8_t *s, int interleave, int dirmode) +{ + uint8_t bufA[D64_SECTOR], bufB[D64_SECTOR], *cnt, *bits; + bamloc_t L; + uint8_t tr; int hint; + if (dirmode) { tr = dir_track(d); hint = (*t == tr) ? *s + interleave : dir_sector0(d); } + else if (*t) { tr = *t; hint = *s + interleave; } + else { tr = (uint8_t)next_data_track(d, 0); hint = 0; } + for (;;) { + if (tr == 0 || tr > bam_tracks(d)) return D64_E_FULL; + if (dirmode || !bam_reserved(d, tr)) { + int r = bam_load(d, tr, &L, bufA, bufB, &cnt, &bits); + if (r) return r; + if (*cnt > 0) { + int n = spt(d, tr); + for (int i = 0; i < n; i++) { + int sec = (hint + i) % n; + if (dirmode && sec == 0) continue; + if (bits[sec / 8] & (1 << (sec % 8))) { + bits[sec / 8] &= (uint8_t)~(1 << (sec % 8)); + (*cnt)--; + r = bam_store(d, &L, bufA, bufB); + if (r) return r; + *t = tr; *s = (uint8_t)sec; + return 0; + } + } + } + } + if (dirmode) return D64_E_FULL; + tr = (uint8_t)next_data_track(d, tr); + hint = 0; + } +} + +VD_COLD static int free_sector(d64_t *d, uint8_t t, uint8_t s) +{ + uint8_t bufA[D64_SECTOR], bufB[D64_SECTOR], *cnt, *bits; + bamloc_t L; + if (t < 1 || t > bam_tracks(d) || s >= spt(d, t)) return D64_E_IO; + int r = bam_load(d, t, &L, bufA, bufB, &cnt, &bits); + if (r) return r; + uint8_t bit = (uint8_t)(1 << (s % 8)), *p = &bits[s / 8]; + if (!(*p & bit)) { *p |= bit; (*cnt)++; } + return bam_store(d, &L, bufA, bufB); +} + +VD_COLD int d64_block_read(d64_t *d, uint8_t t, uint8_t s, uint8_t *buf) { return rd(d, t, s, buf); } +VD_COLD int d64_block_write(d64_t *d, uint8_t t, uint8_t s, const uint8_t *buf) { return wr(d, t, s, buf); } +VD_COLD int d64_block_free(d64_t *d, uint8_t t, uint8_t s) { return d->write && !d->ro ? free_sector(d, t, s) : D64_E_RO; } + +VD_COLD int d64_block_alloc(d64_t *d, uint8_t t, uint8_t s, uint8_t *nt, uint8_t *ns) +{ + uint8_t bufA[D64_SECTOR], bufB[D64_SECTOR], *cnt, *bits; + bamloc_t L; + if (t < 1 || t > bam_tracks(d) || s >= spt(d, t)) return D64_E_IO; + if (!d->write || d->ro) return D64_E_RO; + int r = bam_load(d, t, &L, bufA, bufB, &cnt, &bits); + if (r) return r; + if (bits[s / 8] & (1 << (s % 8))) { + bits[s / 8] &= (uint8_t)~(1 << (s % 8)); + if (*cnt) (*cnt)--; + return bam_store(d, &L, bufA, bufB); + } + /* taken: report the next free sector at or after it (track by track, wrapping), as DOS does with error 65 */ + *nt = *ns = 0; + uint8_t tr = t, first = s + 1; + for (int n = 0; n < bam_tracks(d); n++) { + if (!bam_reserved(d, tr)) { + if ((r = bam_load(d, tr, &L, bufA, bufB, &cnt, &bits))) return r; + for (int k = (n ? 0 : first); k < spt(d, tr); k++) + if (bits[k / 8] & (1 << (k % 8))) { *nt = tr; *ns = (uint8_t)k; return D64_E_EXISTS; } + } + tr = (uint8_t)(tr % bam_tracks(d) + 1); + } + return D64_E_EXISTS; +} + +VD_COLD static int valid_name(const char *n) +{ + size_t l = strlen(n); + if (l < 1 || l > 16) return 0; + for (; *n; n++) if (*n == '*' || *n == '?' || *n == ',' || *n == ':' || *n == '=' || *n == '"') return 0; + return 1; +} + +/* Update directory entry bytes in place. */ +VD_COLD static int dir_update(d64_t *d, uint8_t dt, uint8_t ds, uint8_t slot, const uint8_t *entry30 /* NULL: leave */, + int type, int blocks) +{ + uint8_t b[D64_SECTOR]; + int r = rd(d, dt, ds, b); + if (r) return r; + uint8_t *p = b + slot * 32; + if (entry30) memcpy(p + 2, entry30, 30); + if (type >= 0) p[2] = (uint8_t)type; + if (blocks >= 0) { p[30] = (uint8_t)blocks; p[31] = (uint8_t)(blocks >> 8); } + return wr(d, dt, ds, b); +} + +VD_COLD static int free_chain(d64_t *d, uint8_t t, uint8_t s) +{ + uint8_t b[D64_SECTOR]; + for (int guard = 0; t && guard < chain_limit(d); guard++) { + int r = rd(d, t, s, b); + if (r) break; /* broken chain: free what we can */ + uint8_t nt = b[0], ns = b[1]; + if ((r = free_sector(d, t, s))) return r; + t = nt; s = ns; + } + return 0; +} + +VD_COLD int d64_scratch_entry(d64_t *d, const d64_entry_t *e) +{ + int r; + if (!d->write || d->ro) return D64_E_RO; + if (e->type & 0x40) return D64_E_LOCKED; + if ((r = free_chain(d, e->track, e->sector))) return r; + if ((e->type & 7) == 4 && (r = free_chain(d, e->ss_t, e->ss_s))) return r; /* REL: the side sectors (d81: from the super side sector) */ + return dir_update(d, e->dir_t, e->dir_s, e->dir_slot, NULL, 0, -1); +} + +VD_COLD int d64_scratch(d64_t *d, const char *pat) +{ + d64_dir_t it; d64_entry_t e; + int r = d64_dir_first(d, &it), n = 0; + if (r) return r; + while ((r = d64_dir_next(d, &it, &e)) > 0) { + if (!d64_match(e.name, pat)) continue; + r = d64_scratch_entry(d, &e); + if (r == D64_E_LOCKED) continue; + if (r) return r; + n++; + } + return r < 0 ? r : n; +} + +VD_COLD int d64_rename(d64_t *d, const char *oldn, const char *newn) +{ + d64_entry_t e, x; + if (!valid_name(newn)) return D64_E_NAME; + if (!d->write || d->ro) return D64_E_RO; + int r = d64_find(d, oldn, &e); + if (r < 0) return r; + if (r == 0) return D64_E_NOTFOUND; + if ((r = d64_find(d, newn, &x)) < 0) return r; + if (r == 1) return D64_E_EXISTS; + uint8_t b[D64_SECTOR]; + if ((r = rd(d, e.dir_t, e.dir_s, b))) return r; + uint8_t *p = b + e.dir_slot * 32 + 5; + size_t l = strlen(newn); + memset(p, 0xA0, 16); memcpy(p, newn, l); + return wr(d, e.dir_t, e.dir_s, b); +} + +/* Find a free directory slot, extending the directory if necessary. */ +VD_COLD static int dir_slot_alloc(d64_t *d, uint8_t *dt, uint8_t *ds, uint8_t *slot) +{ + uint8_t b[D64_SECTOR], t, s; + int r = rd(d, dir_track(d), 0, b); + if (r) return r; + t = b[0] ? b[0] : dir_track(d); s = b[0] ? b[1] : dir_sector0(d); + for (int guard = 0; guard < 64; guard++) { + if ((r = rd(d, t, s, b))) return r; + for (int i = 0; i < 8; i++) + if (b[i * 32 + 2] == 0) { *dt = t; *ds = s; *slot = (uint8_t)i; return 0; } + if (b[0] == 0) { /* last dir sector, full: extend */ + uint8_t nt = t, ns = s; + if ((r = alloc_sector(d, &nt, &ns, 3, 1))) return r; + b[0] = nt; b[1] = ns; + if ((r = wr(d, t, s, b))) return r; + memset(b, 0, sizeof b); b[1] = 0xFF; + if ((r = wr(d, nt, ns, b))) return r; + *dt = nt; *ds = ns; *slot = 0; + return 0; + } + t = b[0]; s = b[1]; + } + return D64_E_LOOP; +} + +VD_COLD int d64_create(d64_t *d, const char *name, uint8_t type, int replace, d64_wfile_t *w) +{ + d64_entry_t e; + if (!d->write || d->ro) return D64_E_RO; + if (!valid_name(name)) return D64_E_NAME; + int r = d64_find(d, name, &e); + if (r < 0) return r; + if (r == 1) { + if (!replace) return D64_E_EXISTS; + if ((r = d64_scratch_entry(d, &e))) return r; + } + uint8_t ft = 0, fs = 0, dt, ds, slot; + if ((r = alloc_sector(d, &ft, &fs, 10, 0))) return r; + if ((r = dir_slot_alloc(d, &dt, &ds, &slot))) { free_sector(d, ft, fs); return r; } + + uint8_t ent[30]; + memset(ent, 0, sizeof ent); + ent[0] = type & 7; /* unclosed until close */ + ent[1] = ft; ent[2] = fs; + memset(ent + 3, 0xA0, 16); memcpy(ent + 3, name, strlen(name)); + if ((r = dir_update(d, dt, ds, slot, ent, -1, -1))) { free_sector(d, ft, fs); return r; } + + w->type = type & 7; w->dir_t = dt; w->dir_s = ds; w->dir_slot = slot; + w->first_t = w->cur_t = ft; w->first_s = w->cur_s = fs; + w->pos = 2; w->blocks = 1; + w->app_t = w->app_s = w->app_end = w->app_type = 0; w->app_blocks = 0; + memset(w->buf, 0, sizeof w->buf); + return 0; +} + +VD_COLD int d64_append(d64_t *d, const char *name, d64_wfile_t *w) +{ + d64_entry_t e; + if (!d->write || d->ro) return D64_E_RO; + int r = d64_find(d, name, &e); + if (r < 0) return r; + if (r == 0) return D64_E_NOTFOUND; + if ((e.type & 7) == 4) return D64_E_TYPE; + uint8_t t = e.track, s = e.sector; + uint16_t n = 0; + for (;;) { /* to the last sector of the chain */ + if (++n > chain_limit(d)) return D64_E_LOOP; + if ((r = rd(d, t, s, w->buf))) return r; + if (!w->buf[0]) break; + t = w->buf[0]; s = w->buf[1]; + } + w->type = e.type & 7; w->dir_t = e.dir_t; w->dir_s = e.dir_s; w->dir_slot = e.dir_slot; + w->first_t = e.track; w->first_s = e.sector; w->cur_t = t; w->cur_s = s; + w->pos = (uint16_t)(w->buf[1] < 1 ? 2 : w->buf[1] + 1); + w->blocks = n; + w->app_t = t; w->app_s = s; w->app_end = w->buf[1]; w->app_type = e.type; w->app_blocks = n; + return dir_update(d, e.dir_t, e.dir_s, e.dir_slot, NULL, e.type & 0x7F, -1); /* unclosed while it is written */ +} + +VD_COLD static void abort_write(d64_t *d, d64_wfile_t *w) +{ /* free exactly the sectors we allocated, then drop the directory entry (an append: put the old file back instead) */ + uint8_t t = w->first_t, s = w->first_s, b[D64_SECTOR]; + if (w->app_t) { + if (rd(d, w->app_t, w->app_s, b)) return; + t = b[0]; s = b[1]; /* sectors added since the append began; the last one is cur, not yet written */ + for (int i = w->app_blocks; i < w->blocks && t; i++) { + uint8_t nt = 0, ns = 0, c[D64_SECTOR]; + if (i + 1 < w->blocks && rd(d, t, s, c) == 0) { nt = c[0]; ns = c[1]; } + free_sector(d, t, s); + t = nt; s = ns; + } + b[0] = 0; b[1] = w->app_end; + wr(d, w->app_t, w->app_s, b); + dir_update(d, w->dir_t, w->dir_s, w->dir_slot, NULL, w->app_type, w->app_blocks); + return; + } + for (int i = 0; i < w->blocks; i++) { + uint8_t nt = 0, ns = 0; + if (i + 1 < w->blocks && rd(d, t, s, b) == 0) { nt = b[0]; ns = b[1]; } + free_sector(d, t, s); + if (!nt) break; + t = nt; s = ns; + } + dir_update(d, w->dir_t, w->dir_s, w->dir_slot, NULL, 0, -1); +} + +VD_COLD int d64_write(d64_t *d, d64_wfile_t *w, const uint8_t *data, size_t n) +{ + for (size_t i = 0; i < n; i++) { + if (w->pos == D64_SECTOR) { + uint8_t nt = w->cur_t, ns = w->cur_s; + int r = alloc_sector(d, &nt, &ns, 10, 0); + if (r == 0) { + w->buf[0] = nt; w->buf[1] = ns; + r = wr(d, w->cur_t, w->cur_s, w->buf); + if (r) free_sector(d, nt, ns); /* don't leak the sector we just took */ + } + if (r) { abort_write(d, w); return r; } + w->cur_t = nt; w->cur_s = ns; w->blocks++; + memset(w->buf, 0, sizeof w->buf); + w->pos = 2; + } + w->buf[w->pos++] = data[i]; + } + return (int)n; +} + +VD_COLD int d64_close_write(d64_t *d, d64_wfile_t *w) +{ + w->buf[0] = 0; w->buf[1] = (uint8_t)(w->pos - 1); /* index of last used byte */ + int r = wr(d, w->cur_t, w->cur_s, w->buf); + if (r) return r; + return dir_update(d, w->dir_t, w->dir_s, w->dir_slot, NULL, 0x80 | w->type, w->blocks); +} + +/* ---- REL files ----------------------------------------------------------- */ +#define SS_PER 120 /* data-block pointers in a side sector (bytes 16..255) */ +#define SS_GROUP 6 /* side sectors listed in each one (bytes 4..15): a d64/d71 file, a d81 group */ + +VD_COLD static uint16_t rel_max_ss(const d64_t *d) { return d->fmt == D64_FMT_D81 ? 126 * SS_GROUP : SS_GROUP; } +VD_COLD static uint8_t rel_fill(const d64_rel_t *r, uint32_t off) { return off % r->reclen ? 0x00 : 0xFF; } /* empty records */ + +VD_COLD uint32_t d64_rel_records(const d64_rel_t *r) +{ + if (!r->nblocks || !r->reclen) return 0; + return ((uint32_t)(r->nblocks - 1) * 254 + (r->last > 1 ? r->last - 1 : 0)) / r->reclen; +} + +VD_COLD int d64_rel_flush(d64_t *d, d64_rel_t *r) +{ + if (!r->dirty) return 0; + r->dirty = 0; + return wr(d, r->cur_t, r->cur_s, r->buf); +} + +/* t/s of side sector k (counted over the whole file) */ +VD_COLD static int rel_ss_find(d64_t *d, const d64_rel_t *r, uint16_t k, uint8_t *t, uint8_t *s, uint8_t *tmp) +{ + uint8_t gt = r->ss_t, gs = r->ss_s; + int rr; + if (k + 1 == r->nss) { *t = r->lss_t; *s = r->lss_s; return 0; } + if (d->fmt == D64_FMT_D81) { + if (k / SS_GROUP >= 126) return D64_E_IO; + if ((rr = rd(d, r->ss_t, r->ss_s, tmp))) return rr; + gt = tmp[3 + 2 * (k / SS_GROUP)]; gs = tmp[4 + 2 * (k / SS_GROUP)]; + } else if (k >= SS_GROUP) return D64_E_IO; + if ((rr = rd(d, gt, gs, tmp))) return rr; + *t = tmp[4 + 2 * (k % SS_GROUP)]; *s = tmp[5 + 2 * (k % SS_GROUP)]; + return *t ? 0 : D64_E_IO; +} + +/* bring data block idx into buf */ +VD_COLD static int rel_load(d64_t *d, d64_rel_t *r, uint16_t idx) +{ + uint8_t t, s, tmp[D64_SECTOR]; + int rr; + if (r->cur == idx) return 0; + if (idx >= r->nblocks) return D64_E_IO; + if ((rr = d64_rel_flush(d, r))) return rr; + if (r->cur != 0xFFFF && idx == r->cur + 1 && r->buf[0]) { t = r->buf[0]; s = r->buf[1]; } /* the next one: follow the link */ + else if (idx + 1 == r->nblocks) { t = r->last_t; s = r->last_s; } + else { + if ((rr = rel_ss_find(d, r, (uint16_t)(idx / SS_PER), &t, &s, tmp)) || (rr = rd(d, t, s, tmp))) return rr; + t = tmp[16 + 2 * (idx % SS_PER)]; s = tmp[17 + 2 * (idx % SS_PER)]; + } + r->cur = 0xFFFF; + if ((rr = rd(d, t, s, r->buf))) return rr; + r->cur = idx; r->cur_t = t; r->cur_s = s; + return 0; +} + +VD_COLD int d64_rel_get(d64_t *d, d64_rel_t *r, uint32_t off, uint8_t *b) +{ + int rr = rel_load(d, r, (uint16_t)(off / 254)); + if (rr) return rr; + *b = r->buf[2 + off % 254]; + return 0; +} + +VD_COLD int d64_rel_put(d64_t *d, d64_rel_t *r, uint32_t off, uint8_t b) +{ + if (!d->write || d->ro) return D64_E_RO; + int rr = rel_load(d, r, (uint16_t)(off / 254)); + if (rr) return rr; + r->buf[2 + off % 254] = b; r->dirty = 1; + return 0; +} + +/* block count (data + side sectors + d81 super side sector) and, for the first data block, its t/s into the directory entry */ +VD_COLD static int rel_dir_sync(d64_t *d, const d64_rel_t *r, uint8_t first_t, uint8_t first_s) +{ + uint8_t b[D64_SECTOR]; + int rr = rd(d, r->dir_t, r->dir_s, b); + if (rr) return rr; + uint8_t *p = b + r->dir_slot * 32; + unsigned n = r->nblocks + r->nss + (d->fmt == D64_FMT_D81); + if (first_t) { p[3] = first_t; p[4] = first_s; } + p[30] = (uint8_t)n; p[31] = (uint8_t)(n >> 8); + return wr(d, r->dir_t, r->dir_s, b); +} + +/* Append one data block of empty records, and a side sector (on a d81 maybe a new group) when the last one is full. */ +VD_COLD static int rel_add_block(d64_t *d, d64_rel_t *r) +{ + uint8_t tmp[D64_SECTOR], t = r->last_t, s = r->last_s, nt, ns = 0; + const uint16_t idx = r->nblocks, k = (uint16_t)(idx / SS_PER), j = (uint16_t)(k % SS_GROUP); + const int new_ss = idx && idx % SS_PER == 0; + int rr; + if (new_ss && k >= rel_max_ss(d)) return D64_E_TOOBIG; + if ((rr = d64_rel_flush(d, r))) return rr; + r->cur = 0xFFFF; + if ((rr = alloc_sector(d, &t, &s, 10, 0))) return rr; + nt = t; ns = s; + if (new_ss && (rr = alloc_sector(d, &nt, &ns, 10, 0))) { free_sector(d, t, s); return rr; } + + if (new_ss) { + uint8_t list[2 * SS_GROUP]; + memset(list, 0, sizeof list); + if (j) { if ((rr = rd(d, r->lss_t, r->lss_s, tmp))) return rr; memcpy(list, tmp + 4, sizeof list); } + list[2 * j] = nt; list[2 * j + 1] = ns; + memset(tmp, 0, sizeof tmp); + tmp[1] = 17; tmp[2] = (uint8_t)j; tmp[3] = r->reclen; + memcpy(tmp + 4, list, sizeof list); + tmp[16] = t; tmp[17] = s; + if ((rr = wr(d, nt, ns, tmp))) return rr; + for (uint16_t i = 0; i < j; i++) { /* the others in the group learn of it; the previous one links to it */ + uint8_t ot = list[2 * i], os = list[2 * i + 1]; + if ((rr = rd(d, ot, os, tmp))) return rr; + memcpy(tmp + 4, list, sizeof list); + if (i + 1 == j) { tmp[0] = nt; tmp[1] = ns; } + if ((rr = wr(d, ot, os, tmp))) return rr; + } + if (!j) { /* new d81 group: the last side sector links on, the super side sector lists it */ + if ((rr = rd(d, r->lss_t, r->lss_s, tmp))) return rr; + tmp[0] = nt; tmp[1] = ns; + if ((rr = wr(d, r->lss_t, r->lss_s, tmp))) return rr; + if ((rr = rd(d, r->ss_t, r->ss_s, tmp))) return rr; + tmp[3 + 2 * (k / SS_GROUP)] = nt; tmp[4 + 2 * (k / SS_GROUP)] = ns; + if ((rr = wr(d, r->ss_t, r->ss_s, tmp))) return rr; + } + r->lss_t = nt; r->lss_s = ns; r->nss++; + } else { + uint8_t e = (uint8_t)(16 + 2 * (idx % SS_PER)); + if ((rr = rd(d, r->lss_t, r->lss_s, tmp))) return rr; + tmp[e] = t; tmp[e + 1] = s; tmp[1] = (uint8_t)(e + 1); + if ((rr = wr(d, r->lss_t, r->lss_s, tmp))) return rr; + } + + uint32_t base = (uint32_t)idx * 254, end = (base + 254) / r->reclen * r->reclen; /* the last whole record ends here */ + tmp[0] = 0; tmp[1] = (uint8_t)(end - base + 1); + for (int i = 0; i < 254; i++) tmp[2 + i] = rel_fill(r, base + (uint32_t)i); + if ((rr = wr(d, t, s, tmp))) return rr; + if (idx) { /* the old last block links on; its unused tail becomes empty records */ + if ((rr = rd(d, r->last_t, r->last_s, tmp))) return rr; + for (int i = r->last + 1; i < 256; i++) tmp[i] = rel_fill(r, base - 254 + (uint32_t)(i - 2)); + tmp[0] = t; tmp[1] = s; + if ((rr = wr(d, r->last_t, r->last_s, tmp))) return rr; + } + r->last_t = t; r->last_s = s; r->last = (uint8_t)(end - base + 1); + r->nblocks++; + return rel_dir_sync(d, r, idx ? 0 : t, s); +} + +VD_COLD int d64_rel_extend(d64_t *d, d64_rel_t *r, uint32_t nrec) +{ + if (!d->write || d->ro) return D64_E_RO; + while (d64_rel_records(r) < nrec) { int rr = rel_add_block(d, r); if (rr) return rr; } + return 0; +} + +/* drop a REL file we were making (creation or copy failed) */ +VD_COLD static void rel_discard(d64_t *d, const d64_rel_t *r) +{ + uint8_t b[D64_SECTOR]; + if (rd(d, r->dir_t, r->dir_s, b)) return; + const uint8_t *p = b + r->dir_slot * 32; + d64_entry_t e; + memset(&e, 0, sizeof e); + e.type = 0x84; e.track = p[3]; e.sector = p[4]; e.ss_t = p[21]; e.ss_s = p[22]; + e.dir_t = r->dir_t; e.dir_s = r->dir_s; e.dir_slot = r->dir_slot; + d64_scratch_entry(d, &e); +} + +VD_COLD int d64_rel_create(d64_t *d, const char *name, uint8_t reclen, d64_rel_t *r) +{ + d64_entry_t e; + uint8_t tmp[D64_SECTOR], st = 0, ss = 0, pt = 0, ps = 0, dt, ds, slot; + const int d81 = d->fmt == D64_FMT_D81; + int rr; + if (!d->write || d->ro) return D64_E_RO; + if (!valid_name(name) || reclen < 1 || reclen > 254) return D64_E_NAME; + if ((rr = d64_find(d, name, &e)) < 0) return rr; + if (rr == 1) return D64_E_EXISTS; + if ((rr = alloc_sector(d, &st, &ss, 10, 0))) return rr; /* side sector 0 */ + pt = st; ps = ss; + if (d81 && (rr = alloc_sector(d, &pt, &ps, 10, 0))) { free_sector(d, st, ss); return rr; } /* super side sector */ + if ((rr = dir_slot_alloc(d, &dt, &ds, &slot))) { free_sector(d, st, ss); if (d81) free_sector(d, pt, ps); return rr; } + memset(tmp, 0, sizeof tmp); + tmp[1] = 15; tmp[3] = reclen; tmp[4] = st; tmp[5] = ss; /* no data blocks yet */ + if (!(rr = wr(d, st, ss, tmp)) && d81) { + memset(tmp, 0, sizeof tmp); + tmp[0] = st; tmp[1] = ss; tmp[2] = 0xFE; tmp[3] = st; tmp[4] = ss; + rr = wr(d, pt, ps, tmp); + } + if (!rr) { + memset(tmp, 0, 30); + tmp[0] = 0x84; /* REL files are never "splat": closed from the start */ + memset(tmp + 3, 0xA0, 16); memcpy(tmp + 3, name, strlen(name)); + tmp[19] = d81 ? pt : st; tmp[20] = d81 ? ps : ss; tmp[21] = reclen; + rr = dir_update(d, dt, ds, slot, tmp, -1, -1); + } + if (rr) { free_sector(d, st, ss); if (d81) free_sector(d, pt, ps); return rr; } + memset(r, 0, sizeof *r); + r->dir_t = dt; r->dir_s = ds; r->dir_slot = slot; + r->ss_t = d81 ? pt : st; r->ss_s = d81 ? ps : ss; + r->lss_t = st; r->lss_s = ss; r->nss = 1; + r->reclen = reclen; r->cur = 0xFFFF; + if ((rr = rel_add_block(d, r))) { rel_discard(d, r); return rr; } + return 0; +} + +VD_COLD int d64_rel_open(d64_t *d, const d64_entry_t *e, d64_rel_t *r) +{ + uint8_t tmp[D64_SECTOR], t = e->ss_t, s = e->ss_s; + int rr; + if ((e->type & 7) != 4) return D64_E_TYPE; + memset(r, 0, sizeof *r); + r->dir_t = e->dir_t; r->dir_s = e->dir_s; r->dir_slot = e->dir_slot; + r->ss_t = e->ss_t; r->ss_s = e->ss_s; r->reclen = e->reclen; r->cur = 0xFFFF; + if (!r->reclen) return D64_E_IO; + if (d->fmt == D64_FMT_D81) { if ((rr = rd(d, t, s, tmp))) return rr; t = tmp[0]; s = tmp[1]; } + while (t) { /* count the data blocks through the side sectors */ + if (r->nss >= rel_max_ss(d)) return D64_E_LOOP; + if ((rr = rd(d, t, s, tmp))) return rr; + uint16_t n = tmp[0] ? SS_PER : (uint16_t)(tmp[1] >= 17 ? (tmp[1] - 15) / 2 : 0); + if (n > SS_PER) n = SS_PER; + if (n) { r->last_t = tmp[14 + 2 * n]; r->last_s = tmp[15 + 2 * n]; } + r->nblocks = (uint16_t)(r->nblocks + n); r->nss++; + r->lss_t = t; r->lss_s = s; + t = tmp[0]; s = tmp[1]; + } + if (r->nblocks) { + if ((rr = rd(d, r->last_t, r->last_s, tmp))) return rr; + r->last = tmp[0] ? 255 : tmp[1]; + } + return 0; +} + +VD_COLD static int rel_copy(d64_t *d, const char *newname, const d64_entry_t *e) +{ + d64_rel_t a, b; + int rr; + if ((rr = d64_rel_open(d, e, &a)) || (rr = d64_rel_create(d, newname, a.reclen, &b))) return rr; + while (!rr && b.nblocks < a.nblocks) rr = rel_add_block(d, &b); + for (uint16_t i = 0; !rr && i < a.nblocks; i++) + if (!(rr = rel_load(d, &a, i)) && !(rr = rel_load(d, &b, i))) { + memcpy(b.buf + 2, a.buf + 2, 254); + if (i + 1 == a.nblocks) { b.buf[1] = a.last; b.last = a.last; } + b.dirty = 1; + } + if (!rr) rr = d64_rel_flush(d, &b); + if (rr) rel_discard(d, &b); + return rr; +} + +/* d64_find, but never the entry being written (a pattern could match the new file's own splat entry) */ +VD_COLD static int find_except(d64_t *d, const char *pat, const d64_wfile_t *w, d64_entry_t *e) +{ + d64_dir_t it; + int r = d64_dir_first(d, &it); + if (r) return r; + while ((r = d64_dir_next(d, &it, e)) > 0) + if (((e->type & 7) != 0 || (e->type & 0x80)) && d64_match(e->name, pat) + && !(w && e->dir_t == w->dir_t && e->dir_s == w->dir_s && e->dir_slot == w->dir_slot)) return 1; + return r; +} + +VD_COLD int d64_copy(d64_t *d, const char *newname, const char *const *src, int nsrc) +{ + d64_entry_t e; + d64_file_t f; + d64_wfile_t w; + uint8_t type = 0, chunk[64]; + int r; + if (!d->write || d->ro) return D64_E_RO; + if (nsrc < 1 || !valid_name(newname)) return D64_E_NAME; + for (int i = 0; i < nsrc; i++) { /* every source must exist before anything is written */ + if ((r = find_except(d, src[i], NULL, &e)) < 0) return r; + if (r == 0) return D64_E_NOTFOUND; + if ((e.type & 7) == 4 && nsrc > 1) return D64_E_TYPE; /* REL: records + side sectors, not a byte stream to join */ + if (i == 0) type = e.type & 7; + } + if (type == 4) return rel_copy(d, newname, &e); + if ((r = d64_create(d, newname, type, 0, &w))) return r; + for (int i = 0; i < nsrc; i++) { + if ((r = find_except(d, src[i], &w, &e)) <= 0) { abort_write(d, &w); return r ? r : D64_E_NOTFOUND; } + d64_file_open(d, &e, &f); + while ((r = d64_file_read(d, &f, chunk, sizeof chunk)) > 0) + if ((r = d64_write(d, &w, chunk, (size_t)r)) < 0) return r; /* d64_write already removed the new file */ + if (r < 0) { abort_write(d, &w); return D64_E_IO; } + } + return d64_close_write(d, &w); +} + +/* clear: zero every sector that is not header / BAM / first directory sector (a full format); 0 for an image known to be blank */ +VD_COLD static int format(d64_t *d, const char *name, const char *id, int clear) +{ + uint8_t b[D64_SECTOR]; + uint8_t idb[2]; + const int d81 = d->fmt == D64_FMT_D81, d71 = d->fmt == D64_FMT_D71; + const uint8_t dt = dir_track(d); + const int name_off = d81 ? 0x04 : 0x90, id_off = d81 ? 0x16 : 0xA2; + if (!d->write || d->ro) return D64_E_RO; + if (!valid_name(name)) return D64_E_NAME; + if (id) { + if (strlen(id) != 2) return D64_E_NAME; + idb[0] = (uint8_t)id[0]; idb[1] = (uint8_t)id[1]; + memset(b, 0, sizeof b); + if (clear) for (uint8_t t = 1; t <= d->tracks; t++) + for (int sct = 0; sct < spt(d, t); sct++) + if (t != dt || sct > (d81 ? 3 : 1)) { int r = wr(d, t, (uint8_t)sct, b); if (r) return r; } + } else { + int r = rd(d, dt, 0, b); + if (r) return r; + idb[0] = b[id_off]; idb[1] = b[id_off + 1]; + } + /* BAM: every sector free, then the system sectors taken */ + uint8_t bam[3][D64_SECTOR]; /* d64/d71: [0] = 18/0, [1] = 53/0. d81: [1] = 40/1, [2] = 40/2 */ + memset(bam, 0, sizeof bam); + for (uint8_t t = 1; t <= bam_tracks(d); t++) { + bamloc_t L; + bam_loc(d, t, &L); + uint8_t *cs = bam[d81 ? L.cs : (L.ct == 18 ? 0 : 1)], *bs = bam[d81 ? L.bs : (L.bt == 18 ? 0 : 1)]; + int n = spt(d, t); + cs[L.co] = (uint8_t)n; + for (int sct = 0; sct < n; sct++) bs[L.bo + sct / 8] |= (uint8_t)(1 << (sct % 8)); + } + if (d81) { + bamloc_t L; bam_loc(d, 40, &L); + bam[1][L.co] = 36; bam[1][L.bo] &= 0xF0; /* 40/0..40/3 in use */ + for (int k = 1; k <= 2; k++) { + bam[k][0] = k == 1 ? 40 : 0; bam[k][1] = k == 1 ? 2 : 0xFF; + bam[k][2] = 'D'; bam[k][3] = 0xBB; bam[k][4] = idb[0]; bam[k][5] = idb[1]; bam[k][6] = 0xC0; + } + memset(b, 0, sizeof b); + b[0] = 40; b[1] = 3; b[2] = 'D'; + memset(b + 0x04, 0xA0, 16); memcpy(b + 0x04, name, strlen(name)); + b[0x14] = b[0x15] = 0xA0; + b[0x16] = idb[0]; b[0x17] = idb[1]; + b[0x18] = 0xA0; b[0x19] = '3'; b[0x1A] = 'D'; b[0x1B] = b[0x1C] = 0xA0; + int r = wr(d, 40, 0, b); if (r) return r; + if ((r = wr(d, 40, 1, bam[1]))) return r; + if ((r = wr(d, 40, 2, bam[2]))) return r; + memset(b, 0, sizeof b); b[1] = 0xFF; + return wr(d, 40, 3, b); + } + uint8_t *h = bam[0]; /* header sector 18/0 = the first BAM sector */ + h[0] = 18; h[1] = 1; h[2] = 'A'; h[3] = d71 ? 0x80 : 0; + bamloc_t L; bam_loc(d, 18, &L); + h[L.co] -= 2; h[L.bo] &= 0xFC; /* 18/0 and 18/1 in use */ + if (d71) { + bam_loc(d, 53, &L); + bam[0][L.co] = 0; /* track 53 holds the side-2 BAM: no file sectors */ + memset(bam[1] + L.bo, 0, 3); + } + memset(h + name_off, 0xA0, 16); memcpy(h + name_off, name, strlen(name)); + h[0xA0] = h[0xA1] = 0xA0; + h[0xA2] = idb[0]; h[0xA3] = idb[1]; + h[0xA4] = 0xA0; h[0xA5] = '2'; h[0xA6] = 'A'; + h[0xA7] = h[0xA8] = h[0xA9] = h[0xAA] = 0xA0; + int r = wr(d, 18, 0, h); + if (r) return r; + if (d71 && (r = wr(d, 53, 0, bam[1]))) return r; + memset(b, 0, sizeof b); b[1] = 0xFF; + return wr(d, 18, 1, b); +} + +VD_COLD int d64_format(d64_t *d, const char *name, const char *id) { return format(d, name, id, 1); } +VD_COLD int d64_format_blank(d64_t *d, const char *name, const char *id) { return id ? format(d, name, id, 0) : D64_E_NAME; } + +/* ---- validate ------------------------------------------------------------ */ +typedef struct { uint8_t b[2][D64_SECTOR]; uint8_t t0, s0, t1, s1; } vbam_t; /* the BAM sectors (d64: one; d71: 18/0, 53/0; d81: 40/1, 40/2) */ + +VD_COLD static uint8_t *vbam_at(vbam_t *v, uint8_t t, uint8_t s) { return t == v->t0 && s == v->s0 ? v->b[0] : v->b[1]; } + +VD_COLD static void vbam_use(d64_t *d, vbam_t *v, uint8_t t, uint8_t s) +{ + bamloc_t L; + if (t < 1 || t > bam_tracks(d) || s >= spt(d, t)) return; /* 40-track images: tracks 36-40 are not in the BAM */ + bam_loc(d, t, &L); + uint8_t *bits = vbam_at(v, L.bt, L.bs) + L.bo, *cnt = vbam_at(v, L.ct, L.cs) + L.co, bit = (uint8_t)(1 << (s % 8)); + if (bits[s / 8] & bit) { bits[s / 8] &= (uint8_t)~bit; if (*cnt) (*cnt)--; } +} + +/* mark a whole chain used; stops at an illegal link or after chain_limit sectors */ +VD_COLD static void vbam_chain(d64_t *d, vbam_t *v, uint8_t t, uint8_t s, uint8_t *tmp) +{ + for (uint16_t n = 0; t && n < chain_limit(d); n++) { + if (rd(d, t, s, tmp)) return; + vbam_use(d, v, t, s); + t = tmp[0]; s = tmp[1]; + } +} + +VD_COLD int d64_validate(d64_t *d) +{ + vbam_t v; + uint8_t tmp[D64_SECTOR], dirb[D64_SECTOR]; + const int d81 = d->fmt == D64_FMT_D81, d71 = d->fmt == D64_FMT_D71; + const uint8_t dt = dir_track(d); + int r, deleted = 0; + if (!d->write || d->ro) return D64_E_RO; + v.t0 = dt; v.s0 = d81 ? 1 : 0; v.t1 = d81 ? 40 : 53; v.s1 = d81 ? 2 : 0; + if ((r = rd(d, v.t0, v.s0, v.b[0]))) return r; + if ((d81 || d71) && (r = rd(d, v.t1, v.s1, v.b[1]))) return r; + for (uint8_t t = 1; t <= bam_tracks(d); t++) { /* everything free (d71 track 53 stays all used, as formatted) */ + bamloc_t L; + bam_loc(d, t, &L); + uint8_t *bits = vbam_at(&v, L.bt, L.bs) + L.bo, n = (uint8_t)spt(d, t); + if (d71 && t == 53) { vbam_at(&v, L.ct, L.cs)[L.co] = 0; memset(bits, 0, 3); continue; } + vbam_at(&v, L.ct, L.cs)[L.co] = n; + memset(bits, 0, L.nb); + for (int k = 0; k < n; k++) bits[k / 8] |= (uint8_t)(1 << (k % 8)); + } + vbam_use(d, &v, dt, 0); /* header (d81: and the two BAM sectors) */ + if (d81) { vbam_use(d, &v, 40, 1); vbam_use(d, &v, 40, 2); } + if ((r = rd(d, dt, 0, tmp))) return r; + uint8_t t = tmp[0] ? tmp[0] : dt, s = tmp[0] ? tmp[1] : dir_sector0(d); + for (int guard = 0; t && guard < 64; guard++) { /* the directory and every file in it */ + if ((r = rd(d, t, s, dirb))) return r; + vbam_use(d, &v, t, s); + int dirty = 0; + for (int i = 0; i < 8; i++) { + uint8_t *p = dirb + i * 32; + if (!p[2]) continue; + if (!(p[2] & 0x80)) { p[2] = 0; dirty = 1; deleted++; continue; } /* unclosed: its chain is not trusted, the entry goes */ + vbam_chain(d, &v, p[3], p[4], tmp); + if ((p[2] & 7) == 4) vbam_chain(d, &v, p[21], p[22], tmp); /* REL side sectors (d81: from the super side sector) */ + } + if (dirty && (r = wr(d, t, s, dirb))) return r; + t = dirb[0]; s = dirb[1]; + } + if ((r = wr(d, v.t0, v.s0, v.b[0]))) return r; + if ((d81 || d71) && (r = wr(d, v.t1, v.s1, v.b[1]))) return r; + return deleted; +} + +VD_COLD int d64_lock(d64_t *d, const char *pat, int lock) +{ + d64_dir_t it; d64_entry_t e; + int r = d64_dir_first(d, &it), n = 0; + if (!d->write || d->ro) return D64_E_RO; + if (r) return r; + while ((r = d64_dir_next(d, &it, &e)) > 0) { + if (!(e.type & 0x80) || !d64_match(e.name, pat)) continue; + uint8_t type = lock < 0 ? (uint8_t)(e.type ^ 0x40) : lock ? (uint8_t)(e.type | 0x40) : (uint8_t)(e.type & ~0x40); + if (type == e.type) continue; + if ((r = dir_update(d, e.dir_t, e.dir_s, e.dir_slot, NULL, type, -1))) return r; + n++; + } + return r < 0 ? r : n; +} + +/* ---- BASIC listing ------------------------------------------------------ */ +VD_COLD static void put(d64_bas_t *b, uint8_t c) +{ + if (b->len >= b->cap) { b->ok = 0; return; } + b->p[b->len++] = c; +} +VD_COLD static void line_begin(d64_bas_t *b, uint16_t num) +{ + b->link_at = (uint16_t)b->len; + put(b, 0); put(b, 0); /* link patched in line_end */ + put(b, num & 0xFF); put(b, num >> 8); +} +VD_COLD static void line_end(d64_bas_t *b) +{ + put(b, 0); + uint16_t next = (uint16_t)(0x0401 + b->base + b->len - 2); /* -2: load address bytes come first */ + if (b->ok) { b->p[b->link_at] = next & 0xFF; b->p[b->link_at + 1] = next >> 8; } +} +VD_COLD static void puts_(d64_bas_t *b, const char *s) { while (*s) put(b, (uint8_t)*s++); } + +VD_COLD void d64_bas_begin_at(d64_bas_t *b, uint8_t *out, size_t cap, size_t base) +{ + b->p = out; b->cap = cap; b->len = 0; b->base = base; b->ok = 1; b->link_at = 0; + if (base == 0) { put(b, 0x01); put(b, 0x04); } /* load address $0401 */ +} + +VD_COLD void d64_bas_begin(d64_bas_t *b, uint8_t *out, size_t cap) { d64_bas_begin_at(b, out, cap, 0); } + +VD_COLD void d64_bas_header(d64_bas_t *b, const char *name, const char *id, const char *dos) +{ + line_begin(b, 0); + put(b, 0x12); put(b, '"'); /* reverse video */ + int n = (int)strlen(name); + for (int i = 0; i < 16; i++) put(b, i < n ? (uint8_t)name[i] : 0x20); + put(b, '"'); put(b, ' '); puts_(b, id); put(b, ' '); puts_(b, dos); + line_end(b); +} + +VD_COLD void d64_bas_entry(d64_bas_t *b, uint16_t blocks, const char *name, const char *type3, int closed, int locked) +{ + line_begin(b, blocks); + int pad = blocks < 10 ? 3 : blocks < 100 ? 2 : blocks < 1000 ? 1 : 0; + for (int i = 0; i < pad; i++) put(b, ' '); + put(b, '"'); + int l = (int)strlen(name); + for (int i = 0; i < l; i++) put(b, (uint8_t)name[i]); + put(b, '"'); + for (int i = l; i < 16; i++) put(b, ' '); + put(b, closed ? ' ' : '*'); + puts_(b, type3); + put(b, locked ? '<' : ' '); + line_end(b); +} + +VD_COLD void d64_bas_footer(d64_bas_t *b, uint16_t free_blocks) +{ + line_begin(b, free_blocks); + static const char text[] D64_FLASH = "BLOCKS FREE. "; + puts_(b, text); + line_end(b); +} + +VD_COLD size_t d64_bas_end(d64_bas_t *b) +{ + put(b, 0); put(b, 0); /* end of program */ + return b->ok ? b->len : 0; +} + +static const char TYPES[8][4] D64_FLASH = { "DEL", "SEQ", "PRG", "USR", "REL", "???", "???", "???" }; + +VD_COLD size_t d64_build_dir(d64_t *d, const char *pattern, uint8_t *out, size_t cap) +{ + d64_bas_t b; + char name[17], id[3], dos[3]; + d64_bas_begin(&b, out, cap); + if (d64_header(d, name, id, dos)) return 0; + d64_bas_header(&b, name, id, dos); + + d64_dir_t it; d64_entry_t e; + if (d64_dir_first(d, &it)) return 0; + int r; + while ((r = d64_dir_next(d, &it, &e)) > 0) { + if (pattern && !d64_match(e.name, pattern)) continue; + d64_bas_entry(&b, e.blocks, e.name, TYPES[e.type & 7], (e.type & 0x80) != 0, (e.type & 0x40) != 0); + } + if (r < 0) return 0; + + int fr = d64_blocks_free(d); + if (fr < 0) return 0; + d64_bas_footer(&b, (uint16_t)fr); + return d64_bas_end(&b); +} + +VD_COLD size_t d64_sizeof_d64(void) { return sizeof(d64_t); } +VD_COLD size_t d64_sizeof_mem(void) { return sizeof(d64_mem_t); } +VD_COLD size_t d64_sizeof_dir(void) { return sizeof(d64_dir_t); } +VD_COLD size_t d64_sizeof_wfile(void) { return sizeof(d64_wfile_t); } +VD_COLD size_t d64_sizeof_file(void) { return sizeof(d64_file_t); } +VD_COLD size_t d64_sizeof_entry(void) { return sizeof(d64_entry_t); } +VD_COLD size_t d64_sizeof_rel(void) { return sizeof(d64_rel_t); } diff --git a/Source/Teensy/DiskImage/d64.h b/Source/Teensy/DiskImage/d64.h new file mode 100644 index 00000000..a97b8684 --- /dev/null +++ b/Source/Teensy/DiskImage/d64.h @@ -0,0 +1,215 @@ +/* SPDX-License-Identifier: MIT */ +/* Disk image engine (read + write) for .d64 (1541, 35/40 tracks), .d71 (1571, 70 tracks) and .d81 (1581, 80 tracks). + * Portable C99, no malloc: all state lives in caller structs and sectors are fetched through a callback, so the same + * code runs on the Teensy (SD/USB file backend) and on the host (tests, VICE mock). Names keep the d64_ prefix. */ +#ifndef D64_H +#define D64_H +#include +#include + +/* Placement: VD_COLD for code (nothing here is reachable from a bus interrupt), D64_FLASH for constant tables. + * The firmware sets them to FLASHMEM / PROGMEM (DiskImage.h); on a host they are empty. */ +#ifndef VD_COLD +#define VD_COLD +#endif +#ifndef D64_FLASH +#define D64_FLASH +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define D64_SECTOR 256 + +/* Fetch one 256-byte sector. Return 0 on success. */ +typedef int (*d64_read_fn)(void *ctx, uint8_t track, uint8_t sector, uint8_t *buf); + +/* Store one 256-byte sector. Return 0 on success. NULL = read-only disk. */ +typedef int (*d64_write_fn)(void *ctx, uint8_t track, uint8_t sector, const uint8_t *buf); + +enum { + D64_OK = 0, + D64_E_IO = -1, /* sector read/write failed or link out of range */ + D64_E_LOOP = -2, /* chain loop */ + D64_E_FULL = -3, /* disk (or directory) full -> DOS 72 */ + D64_E_EXISTS = -4, /* file exists -> DOS 63 */ + D64_E_NAME = -5, /* bad file name -> DOS 33 */ + D64_E_RO = -6, /* read-only disk -> DOS 26 */ + D64_E_LOCKED = -7, /* file is locked -> DOS 26 */ + D64_E_NOTFOUND = -8, /* file not found -> DOS 62 */ + D64_E_TYPE = -9, /* file type mismatch -> DOS 64 */ + D64_E_TOOBIG = -10, /* REL file at its size limit -> DOS 52 */ + D64_E_BADSEC = -11 /* the image's error table marks the sector bad: DOS code in d64_t.bad (20-24, 27, 29, 74) */ +}; + +enum { D64_FMT_D64 = 0, D64_FMT_D71 = 1, D64_FMT_D81 = 2 }; + +typedef struct { + d64_read_fn read; + d64_write_fn write; + void *ctx; + uint8_t tracks; /* 35 or 40 (d64), 70 (d71), 80 (d81) */ + uint8_t fmt; /* D64_FMT_* */ + uint8_t ro; /* write protect set from the C64 (W-1): writes fail as on a read-only image; d64_open clears it */ + /* Error table (the byte per sector some images carry after the data, as written by copiers / VICE): NULL = none. A read of a + * sector whose byte is a read error fails with D64_E_BADSEC, after the data was fetched into the buffer (as VICE does), and the + * first such failure is kept in bad / bad_t / bad_s until the caller clears bad. Writes ignore the table. d64_open clears it. */ + const uint8_t *errtab; + uint8_t bad, bad_t, bad_s; +} d64_t; + +typedef struct { + uint8_t type; /* raw type byte: bit7 closed, bit6 locked, low 3 = DEL/SEQ/PRG/USR/REL */ + uint8_t track, sector; /* first data sector */ + uint16_t blocks; + char name[17]; /* PETSCII, $A0 padding stripped, NUL terminated */ + uint8_t dir_t, dir_s, dir_slot; /* where this entry lives (for update/scratch) */ + uint8_t ss_t, ss_s, reclen; /* REL: first side sector (D81: super side sector), record length */ +} d64_entry_t; + +typedef struct { + uint8_t track, sector; /* next directory sector in chain */ + uint8_t cur_t, cur_s; /* sector held in buf */ + uint8_t slot; /* next entry (0-7) in buf */ + uint8_t guard; + uint8_t buf[D64_SECTOR]; +} d64_dir_t; + +typedef struct { + uint8_t next_t, next_s; /* next sector in chain (0,x = none) */ + uint8_t loaded; /* buf holds a sector */ + uint16_t pos, end; /* read cursor / one past last valid byte in buf */ + uint16_t guard; + uint8_t buf[D64_SECTOR]; +} d64_file_t; + +typedef struct { + uint8_t type; /* DEL/SEQ/PRG/USR (low 3 bits) */ + uint8_t dir_t, dir_s, dir_slot; + uint8_t first_t, first_s; + uint8_t cur_t, cur_s; /* sector being filled */ + uint16_t pos; /* next free index in buf (2..256) */ + uint16_t blocks; /* sectors allocated so far */ + uint8_t app_t, app_s; /* append: the file's old last sector (0 = a new file), */ + uint8_t app_end, app_type; /* its old last-byte index and the old directory type byte, */ + uint16_t app_blocks; /* and its old length: a failed append puts all of it back */ + uint8_t buf[D64_SECTOR]; +} d64_wfile_t; + +/* Relative (REL) file: fixed-length records over a normal data chain, indexed by side sectors. + * d64/d71: up to 6 side sectors (720 data blocks); each holds its number, the record length, the list of all 6 and + * 120 data-block pointers. d81: a super side sector (in the directory entry) points to up to 126 such groups. + * New records are $FF followed by zeros; growing the file fills its last block with whole empty records, as DOS does. + * One data block is cached in buf (written back by d64_rel_flush or when another block is needed). */ +typedef struct { + uint8_t dir_t, dir_s, dir_slot; + uint8_t ss_t, ss_s; /* first side sector (d81: super side sector) */ + uint8_t lss_t, lss_s; /* last side sector */ + uint8_t last_t, last_s; /* last data block */ + uint8_t reclen; + uint8_t last; /* byte 1 of the last data block: index of the last used byte */ + uint16_t nblocks, nss; /* data blocks, side sectors (without the super side sector) */ + uint16_t cur; /* block index held in buf, 0xFFFF = none */ + uint8_t cur_t, cur_s, dirty; + uint8_t buf[D64_SECTOR]; +} d64_rel_t; + +/* Memory-image backend (host tests / mock). Writable if data is. */ +typedef struct { uint8_t *data; size_t len; } d64_mem_t; +void d64_mem_init(d64_mem_t *m, uint8_t *data, size_t len); +int d64_open_mem(d64_t *d, d64_mem_t *m); /* image size selects the format */ +int d64_open(d64_t *d, d64_read_fn rd, void *ctx, uint8_t tracks); /* tracks: 35/40 = d64, 70 = d71, 80 = d81 */ +int d64_open_size(d64_t *d, d64_read_fn rd, void *ctx, size_t file_size); /* format from the file size (error bytes allowed); -1 if too small */ +/* Error table: d64_errtab_offset = where it starts in a file of this size (-1: the file has none; after d64_open_size), d64_sectors = + * the image's sector count (= the table's length). d64_open_mem sets errtab itself. d64_errcode: an error byte -> DOS code, 0 = no + * read error (1 = OK; 2..6, 9, 11, 15 = 20..24, 27, 29, 74; the write errors 25 / 26 / 28 and unknown bytes read fine). */ +long d64_errtab_offset(const d64_t *d, size_t file_size); +int d64_sectors(const d64_t *d); +int d64_errcode(uint8_t errbyte); + +int d64_sectors_in_track(const d64_t *d, uint8_t track); /* 0 if invalid */ +long d64_sector_offset(const d64_t *d, uint8_t track, uint8_t sector); /* byte offset in image, -1 if invalid */ + +/* Disk header: name (16 PETSCII, stripped), id (2), dos type (2), NUL terminated. */ +int d64_header(d64_t *d, char name[17], char id[3], char dos[3]); +int d64_blocks_free(d64_t *d); /* 1541 convention: track 18 not counted */ + +int d64_dir_first(d64_t *d, d64_dir_t *it); +int d64_dir_next(d64_t *d, d64_dir_t *it, d64_entry_t *e); /* 1 entry, 0 end, <0 error */ + +/* CBM wildcard match: '?' any one char, '*' rest of name. */ +int d64_match(const char *name, const char *pattern); +int d64_find(d64_t *d, const char *pattern, d64_entry_t *e); /* 1 found, 0 not found, <0 error */ + +int d64_file_open(d64_t *d, const d64_entry_t *e, d64_file_t *f); +int d64_file_read(d64_t *d, d64_file_t *f, uint8_t *out, size_t n); /* bytes read; 0 = EOF; <0 error */ +int d64_file_eof(const d64_file_t *f); /* 1 when no more bytes will follow */ + +/* BASIC directory listing (load address $0401), optional name pattern (NULL = all). + * Returns length written, or 0 if cap is too small. */ +size_t d64_build_dir(d64_t *d, const char *pattern, uint8_t *out, size_t cap); + +/* ---- write support ---- + * d64_create allocates the first sector and a directory slot (file shows as + * unclosed "splat" until d64_close_write). replace!=0 scratches an existing + * file of that name first (DOS "@:"). */ +int d64_create(d64_t *d, const char *name, uint8_t type, int replace, d64_wfile_t *w); +int d64_write(d64_t *d, d64_wfile_t *w, const uint8_t *data, size_t n); /* n or <0; on D64_E_FULL the file is removed */ +int d64_close_write(d64_t *d, d64_wfile_t *w); +/* DOS ",A": reopen a file for writing at its end (the entry shows as unclosed until d64_close_write). D64_E_NOTFOUND, + * D64_E_TYPE for a REL file. On D64_E_FULL the file is put back as it was before the append. */ +int d64_append(d64_t *d, const char *name, d64_wfile_t *w); +int d64_scratch_entry(d64_t *d, const d64_entry_t *e); /* frees chain, marks entry deleted */ +int d64_scratch(d64_t *d, const char *pattern); /* returns files scratched or <0 */ +int d64_rename(d64_t *d, const char *oldname, const char *newname); +int d64_format(d64_t *d, const char *name, const char *id); /* id==NULL: quick (BAM + directory only) */ +int d64_format_blank(d64_t *d, const char *name, const char *id); /* an all-zero new image: header, BAM, first directory sector only */ +/* DOS "V": rebuild the BAM from the directory, the data chains and REL side sectors; unclosed (splat) files are deleted. + * Returns the number of files deleted, or <0. A broken chain is followed as far as it is valid. */ +int d64_validate(d64_t *d); +/* lock flag (bit 6 of the type: no scratch) of every closed file matching pattern: lock=1 set, 0 clear, -1 toggle. + * Returns the number of files changed, or <0. */ +int d64_lock(d64_t *d, const char *pattern, int lock); +/* DOS "C:new=old1,old2,...": new file, type of the first source, holding the sources one after the other. Sources are + * patterns (first match); D64_E_NOTFOUND if one is missing, D64_E_EXISTS if newname is taken, D64_E_TYPE for a REL source + * among several. On failure nothing is left behind (D64_E_FULL included). */ +int d64_copy(d64_t *d, const char *newname, const char *const *src, int nsrc); + +/* ---- REL files ---- */ +int d64_rel_create(d64_t *d, const char *name, uint8_t reclen, d64_rel_t *r); /* reclen 1..254; one block of empty records */ +int d64_rel_open(d64_t *d, const d64_entry_t *e, d64_rel_t *r); +uint32_t d64_rel_records(const d64_rel_t *r); +int d64_rel_extend(d64_t *d, d64_rel_t *r, uint32_t nrec); /* make records 1..nrec exist; D64_E_TOOBIG / D64_E_FULL */ +int d64_rel_get(d64_t *d, d64_rel_t *r, uint32_t off, uint8_t *b); /* byte at offset off of the record data */ +int d64_rel_put(d64_t *d, d64_rel_t *r, uint32_t off, uint8_t b); +int d64_rel_flush(d64_t *d, d64_rel_t *r); + +/* ---- raw blocks (direct access: U1/U2, B-A, B-F) ---- + * Sector numbers are checked against the geometry: D64_E_IO for an illegal track/sector or a failed transfer. */ +int d64_block_read(d64_t *d, uint8_t t, uint8_t s, uint8_t *buf); +int d64_block_write(d64_t *d, uint8_t t, uint8_t s, const uint8_t *buf); /* D64_E_RO on a read-only image */ +/* Mark t/s used in the BAM. 0 = allocated; D64_E_EXISTS = already used, nt and ns = the next free sector (0/0 = none); + * D64_E_IO = illegal. */ +int d64_block_alloc(d64_t *d, uint8_t t, uint8_t s, uint8_t *nt, uint8_t *ns); +int d64_block_free(d64_t *d, uint8_t t, uint8_t s); /* mark t/s free in the BAM */ + +/* Generic BASIC-listing builder (also used for host directory listings). */ +typedef struct { uint8_t *p; size_t cap, len, base; uint16_t link_at; int ok; } d64_bas_t; +void d64_bas_begin(d64_bas_t *b, uint8_t *out, size_t cap); +/* Incremental form: build one line at a time into a small buffer. base = bytes already emitted + * (load address included) so the line link pointers stay right; base==0 also emits the load address. */ +void d64_bas_begin_at(d64_bas_t *b, uint8_t *out, size_t cap, size_t base); +void d64_bas_header(d64_bas_t *b, const char *name, const char *id, const char *dos); +void d64_bas_entry(d64_bas_t *b, uint16_t blocks, const char *name, const char *type3, int closed, int locked); +void d64_bas_footer(d64_bas_t *b, uint16_t free_blocks); +size_t d64_bas_end(d64_bas_t *b); /* length, or 0 if the buffer overflowed */ + +size_t d64_sizeof_d64(void); size_t d64_sizeof_mem(void); +size_t d64_sizeof_dir(void); size_t d64_sizeof_wfile(void); size_t d64_sizeof_file(void); size_t d64_sizeof_entry(void); +size_t d64_sizeof_rel(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/Source/Teensy/tests/disk-image.test.js b/Source/Teensy/tests/disk-image.test.js new file mode 100644 index 00000000..9d30795f --- /dev/null +++ b/Source/Teensy/tests/disk-image.test.js @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +// The shared D64/D71/D81 engine (Source/Teensy/DiskImage), built with the firmware's warnings as C and as C++. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; + +import { commandExists } from '../../../tools/lib/toolchain.mjs'; + +const FIRMWARE_ROOT = path.resolve(import.meta.dirname, '..'); +const HARNESS_SOURCE = path.join(FIRMWARE_ROOT, 'tests/disk_image_harness.c'); + +const NOT_CI = ['', '0', 'false']; +const inCi = !NOT_CI.includes(process.env.CI ?? ''); +const missingTool = ['cc', 'c++'].find((tool) => !commandExists(tool)); +const skip = missingTool && !inCi && `${missingTool} is not on PATH`; + +let harnessPath; +function harness() { + if (harnessPath) return harnessPath; + assert.equal(missingTool, undefined, 'CI installs every tool this test needs, so it must not skip there'); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'disk-image-')); + harnessPath = path.join(dir, 'harness'); + const built = spawnSync('cc', ['-std=c99', '-Wall', '-Wextra', '-Werror', '-o', harnessPath, HARNESS_SOURCE], { encoding: 'utf8' }); + assert.equal(built.status, 0, built.stderr); + const asCpp = spawnSync('c++', ['-x', 'c++', '-std=gnu++17', '-Wall', '-Werror', '-fsyntax-only', HARNESS_SOURCE], { encoding: 'utf8' }); + assert.equal(asCpp.status, 0, `the firmware compiles this as C++: ${asCpp.stderr}`); + return harnessPath; +} + +const runHarness = (...args) => { + const ran = spawnSync(harness(), args, { encoding: 'utf8' }); + assert.equal(ran.status, 0, ran.stderr); + return ran.stdout.trim(); +}; + +// 1541 DOS order: a file starts on the track next to the directory (18, or 40 on a 1581), at sector 0 +const NEW_DISKS = { + d64: 'free=664,661 blocks=3 first=17/0 same=1', + d71: 'free=1328,1325 blocks=3 first=17/0 same=1', + d81: 'free=3160,3157 blocks=3 first=39/0 same=1', +}; + +for (const [kind, expected] of Object.entries(NEW_DISKS)) { + test(`${kind}: a file saved on a blank disk reads back, and takes its blocks from the free count`, { skip }, () => { + assert.equal(runHarness('new', kind), expected); + }); +} + +test('the image kind follows the file size, error bytes and 40-track D64s included', { skip }, () => { + const sizes = { + 174847: 'none', + 174848: 'd64 35', + 175531: 'd64 35', + 196608: 'd64 40', + 197376: 'd64 40', + 349696: 'd71 70', + 351062: 'd71 70', + 819200: 'd81 80', + 822400: 'd81 80', + }; + for (const [size, expected] of Object.entries(sizes)) assert.equal(runHarness('size', size), expected, `${size} bytes`); +}); diff --git a/Source/Teensy/tests/disk_image_harness.c b/Source/Teensy/tests/disk_image_harness.c new file mode 100644 index 00000000..f6d3efe2 --- /dev/null +++ b/Source/Teensy/tests/disk_image_harness.c @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// disk_image_harness new KIND: formats a blank D64, D71 or D81 in memory, saves a 600-byte file on it, reads it +// back, and prints "free=, blocks= first=/ same=<0|1>". +// disk_image_harness size BYTES: prints the format and track count d64_open_size picks for a file of that size, +// or "none". +#include +#include + +#include "../DiskImage/DiskImage.h" + +static uint8_t Image[819200 + 3200]; + +static int NewDisk(const char *Kind) +{ + static const struct { const char *Kind; size_t Size; } Kinds[] = { { "d64", 174848 }, { "d71", 349696 }, { "d81", 819200 } }; + uint8_t Data[600], Back[sizeof Data + 1]; + d64_mem_t Mem; + d64_t Disk; + d64_wfile_t Out; + d64_file_t In; + d64_entry_t Entry; + int Before, After, Got; + size_t Size = 0; + + for (size_t i = 0; i < sizeof Kinds / sizeof Kinds[0]; i++) + { + if (!strcmp(Kind, Kinds[i].Kind)) Size = Kinds[i].Size; + } + if (!Size) return 2; + for (size_t i = 0; i < sizeof Data; i++) Data[i] = (uint8_t)(i * 7 + 1); + + d64_mem_init(&Mem, Image, Size); + if (d64_open_mem(&Disk, &Mem) || d64_format_blank(&Disk, "TEST", "TR")) return 1; + Before = d64_blocks_free(&Disk); + if (d64_create(&Disk, "HELLO", 2, 0, &Out) || d64_write(&Disk, &Out, Data, sizeof Data) != (int)sizeof Data) return 1; + if (d64_close_write(&Disk, &Out)) return 1; + After = d64_blocks_free(&Disk); + if (d64_find(&Disk, "HELLO", &Entry) != 1 || d64_file_open(&Disk, &Entry, &In)) return 1; + Got = d64_file_read(&Disk, &In, Back, sizeof Back); + printf("free=%d,%d blocks=%u first=%u/%u same=%d\n", Before, After, Entry.blocks, Entry.track, Entry.sector, + Got == (int)sizeof Data && !memcmp(Data, Back, sizeof Data)); + return 0; +} + +static int SizeOf(const char *Bytes) +{ + static const char *const Names[] = { "d64", "d71", "d81" }; + d64_t Disk; + + if (d64_open_size(&Disk, NULL, NULL, (size_t)strtoul(Bytes, NULL, 10))) + { + printf("none\n"); + return 0; + } + printf("%s %u\n", Names[Disk.fmt], Disk.tracks); + return 0; +} + +int main(int argc, char **argv) +{ + if (argc == 3 && !strcmp(argv[1], "new")) return NewDisk(argv[2]); + if (argc == 3 && !strcmp(argv[1], "size")) return SizeOf(argv[2]); + return 2; +} diff --git a/docs/Architecture/Disk-Images.md b/docs/Architecture/Disk-Images.md new file mode 100644 index 00000000..187e47e8 --- /dev/null +++ b/docs/Architecture/Disk-Images.md @@ -0,0 +1,34 @@ +# Disk images (D64, D71, D81) + +One engine reads and writes Commodore disk images for every feature that looks inside one: the GEOS support, the +[TR block device](../TR_Block_Device.md), and VDrive, the virtual IEC drive from +[teensyrom-vdrive](https://github.com/wesbiggs/teensyrom-vdrive). It lives in `Source/Teensy/DiskImage/`. + +| File | What it is | +|---|---| +| `d64.h`, `d64.c` | The engine: geometry, BAM, directory, files (read, write, append, scratch, rename, REL), format, validate, raw sector access. Portable C99 that also compiles as C++; no `malloc`, all state in caller structs. | +| `DiskImage.h` | What the firmware and the host harnesses include. It places the code in flash (`FLASHMEM`) and the tables in `PROGMEM`, and pulls `d64.c` in once. | + +The engine came from teensyrom-vdrive's `d64/` (commit `b74ce37`), where it is tested against VICE's `c1541`. The +copy here differs only in what the firmware needs: the SPDX lines, the placement macros moving into `d64.h`, and its +three constant tables marked `D64_FLASH`. Changes to the engine are made here, and teensyrom-vdrive takes them from +here. + +## How a caller uses it + +The engine never touches a file. A caller gives it a sector reader (and a writer, or none for a read-only image) +that turns a track and sector into a read of 256 bytes; `d64_sector_offset()` gives the byte offset in the image +file. `d64_open_size()` picks the kind from the file size, taking any size from one row's first number up to the +next row as that row's kind: + +| Size | Kind | +|---|---| +| 174848, or 175531 with error bytes | D64, 35 tracks | +| 196608, or 197376 with error bytes | D64, 40 tracks | +| 349696, or 351062 with error bytes | D71 | +| 819200, or 822400 with error bytes | D81 | + +## Tests + +`Source/Teensy/tests/disk-image.test.js` builds the engine with the harness flags as C and as C++, saves and reads a +file on a blank image of each kind, and checks the size table above. diff --git a/docs/Architecture/Overview.md b/docs/Architecture/Overview.md index 2ccf8e06..fbb69b53 100644 --- a/docs/Architecture/Overview.md +++ b/docs/Architecture/Overview.md @@ -33,6 +33,7 @@ C64 code and Teensy firmware also talk to each other **at runtime** two differen - [Teensy-Firmware.md](Teensy-Firmware.md) — module map, entry point, IO_Handlers pattern, MinimalBoot - [C64-Software.md](C64-Software.md) — MainMenuCRT, SettingsMenu, sub-programs, build pipeline - [Comms-Protocol.md](Comms-Protocol.md) — cartridge register protocol + link to external host protocol +- [Disk-Images.md](Disk-Images.md) — the D64/D71/D81 engine that the GEOS support, the TR block device and VDrive share - [Build-System.md](Build-System.md) — toolchains, versions, dual-boot linking, known gotchas - How to build and flash, step by step: the build skill, [.claude/skills/teensyrom-build/SKILL.md](/.claude/skills/teensyrom-build/SKILL.md) - [Constraints.md](Constraints.md) — hard rules: ISR hot path, memory budgets, startup hooks, toolchain pins From f2e7a9e038aa59ff38120136546a94a33b44336b Mon Sep 17 00:00:00 2001 From: Wes Biggs Date: Mon, 2 Nov 2026 16:07:01 +0000 Subject: [PATCH 2/2] firmware: the block device maps sectors through the shared disk image engine TRBlockImage did its own track/sector arithmetic on top of GEOS's format table, which knows only the three plain sizes. A D64 with the error bytes copiers append, or a 40-track D64, therefore mounted as no image. The backend now opens the image with d64_open_exact and reads and writes through d64_block_read/d64_block_write, so those images mount and the geometry is the one VDrive uses. The error table is left unread: the driver has no status for a bad sector, so it gets the sector's bytes. Files of any other size still mount as no image; the harness now reads one byte past the largest image so an oversized file cannot pass as one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X5t9wWXadFkQgKaCnjreF7 --- Source/Teensy/BlockDevice/TRBlockImage.h | 48 ++++++++++++------- Source/Teensy/DiskImage/DiskImage.h | 2 + Source/Teensy/DiskImage/d64.c | 10 ++++ Source/Teensy/DiskImage/d64.h | 2 + .../Common/IO_Handlers/TRBlockIO.c | 13 +++-- Source/Teensy/tests/disk-image.test.js | 18 +++++++ Source/Teensy/tests/disk_image_harness.c | 13 +++-- Source/Teensy/tests/trblock.test.js | 20 ++++++-- Source/Teensy/tests/trblock_harness.c | 23 +++++---- docs/Architecture/Disk-Images.md | 12 ++++- docs/TR_Block_Device.md | 3 +- 11 files changed, 122 insertions(+), 42 deletions(-) diff --git a/Source/Teensy/BlockDevice/TRBlockImage.h b/Source/Teensy/BlockDevice/TRBlockImage.h index bda1b87d..84b1fc03 100644 --- a/Source/Teensy/BlockDevice/TRBlockImage.h +++ b/Source/Teensy/BlockDevice/TRBlockImage.h @@ -1,38 +1,54 @@ // SPDX-License-Identifier: MIT // A CBM disk image (D64, D71, D81) as the block device's backend: the track and sector the driver -// sends become an offset in the image file. +// sends become an offset in the image file, by the shared engine's geometry (docs/Architecture/Disk-Images.md). #pragma once -#include "../GEOS/GeosDiskPatch.h" +#include "../DiskImage/DiskImage.h" #include "TRBlock.h" #define TRBLOCK_IMAGE_WRITABLE 0x01 +// Set Writable, Context and the callbacks, then TRBlockImageOpen. The engine keeps a pointer to the +// struct, so it stays where it was opened. struct TRBlockImage { - const struct GeosFormat *Format; - bool Writable; + d64_t Disk; + bool Open, Writable; void *Context; bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); bool (*Write)(void *Context, uint32_t Offset, const uint8_t *Block); bool (*Flush)(void *Context); }; -static inline bool TRBlockImageHasSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +FLASHMEM static int TRBlockImageSectorRead(void *Context, uint8_t Track, uint8_t Sector, uint8_t *Block) { - return Track >= 1 && Track <= Format->Tracks && Sector < GeosSectorsOnTrack(Format, Track); + struct TRBlockImage *Image = (struct TRBlockImage *)Context; + return Image->Read(Image->Context, (uint32_t)d64_sector_offset(&Image->Disk, Track, Sector), Block) ? 0 : -1; +} + +FLASHMEM static int TRBlockImageSectorWrite(void *Context, uint8_t Track, uint8_t Sector, const uint8_t *Block) +{ + struct TRBlockImage *Image = (struct TRBlockImage *)Context; + return Image->Write(Image->Context, (uint32_t)d64_sector_offset(&Image->Disk, Track, Sector), Block) ? 0 : -1; +} + +// An error table (one byte per sector after the data) is left unread: a GEOS driver has no status for a +// sector marked bad, so it gets the sector's bytes, as a drive that read it anyway would give them. +FLASHMEM static bool TRBlockImageOpen(struct TRBlockImage *Image, uint64_t Size) +{ + Image->Open = Size <= SIZE_MAX && !d64_open_exact(&Image->Disk, TRBlockImageSectorRead, Image, (size_t)Size); + if (Image->Open && Image->Writable) Image->Disk.write = TRBlockImageSectorWrite; + return Image->Open; } FLASHMEM static uint8_t TRBlockImageTransfer(void *Context, bool Write, uint8_t Track, uint8_t Sector, uint8_t *Block) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; - uint32_t Offset; - if (!Image->Format) return TRBlockNoImage; - if (!TRBlockImageHasSector(Image->Format, Track, Sector)) return TRBlockBadAddress; - Offset = GeosOffset(Image->Format, Track, Sector); - if (!Write) return Image->Read(Image->Context, Offset, Block) ? TRBlockDone : TRBlockReadFailed; + if (!Image->Open) return TRBlockNoImage; + if (d64_sector_offset(&Image->Disk, Track, Sector) < 0) return TRBlockBadAddress; + if (!Write) return d64_block_read(&Image->Disk, Track, Sector, Block) ? TRBlockReadFailed : TRBlockDone; if (!Image->Writable) return TRBlockReadOnly; - return Image->Write(Image->Context, Offset, Block) ? TRBlockDone : TRBlockWriteFailed; + return d64_block_write(&Image->Disk, Track, Sector, Block) ? TRBlockWriteFailed : TRBlockDone; } FLASHMEM static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) @@ -40,10 +56,10 @@ FLASHMEM static uint8_t TRBlockImageInfo(void *Context, uint8_t *Out) struct TRBlockImage *Image = (struct TRBlockImage *)Context; uint32_t Blocks; - if (!Image->Format) return TRBlockNoImage; - Blocks = Image->Format->Size / GEOS_SECTOR_SIZE; + if (!Image->Open) return TRBlockNoImage; + Blocks = (uint32_t)d64_sectors(&Image->Disk); - Out[0] = (uint8_t)(Image->Format - GeosFormats + 1); + Out[0] = (uint8_t)(Image->Disk.fmt + 1); Out[1] = Image->Writable ? TRBLOCK_IMAGE_WRITABLE : 0; Out[2] = (uint8_t)Blocks; Out[3] = (uint8_t)(Blocks >> 8); @@ -55,7 +71,7 @@ FLASHMEM static uint8_t TRBlockImageFlush(void *Context) { struct TRBlockImage *Image = (struct TRBlockImage *)Context; - if (!Image->Format) return TRBlockNoImage; + if (!Image->Open) return TRBlockNoImage; return Image->Flush(Image->Context) ? TRBlockDone : TRBlockWriteFailed; } diff --git a/Source/Teensy/DiskImage/DiskImage.h b/Source/Teensy/DiskImage/DiskImage.h index fca1c7f8..92efbf62 100644 --- a/Source/Teensy/DiskImage/DiskImage.h +++ b/Source/Teensy/DiskImage/DiskImage.h @@ -11,6 +11,8 @@ #ifndef D64_FLASH #define D64_FLASH PROGMEM #endif +#else +#define FLASHMEM #endif #include "d64.h" diff --git a/Source/Teensy/DiskImage/d64.c b/Source/Teensy/DiskImage/d64.c index 870eb073..ca64fe6b 100644 --- a/Source/Teensy/DiskImage/d64.c +++ b/Source/Teensy/DiskImage/d64.c @@ -81,6 +81,16 @@ VD_COLD int d64_open_size(d64_t *d, d64_read_fn rd, void *ctx, size_t size) if (classify(size, &tracks, &fmt)) return -1; return d64_open(d, rd, ctx, tracks); } +VD_COLD int d64_open_exact(d64_t *d, d64_read_fn rd, void *ctx, size_t size) +{ + static const uint8_t tracks[] D64_FLASH = { 35, 40, 70, 80 }; + for (size_t i = 0; i < sizeof tracks; i++) { + d64_open(d, rd, ctx, tracks[i]); + size_t n = (size_t)d64_sectors(d); + if (size == n * D64_SECTOR || size == n * (D64_SECTOR + 1)) return 0; + } + return -1; +} VD_COLD int d64_sectors(const d64_t *d) { int n = 0; diff --git a/Source/Teensy/DiskImage/d64.h b/Source/Teensy/DiskImage/d64.h index a97b8684..f87abcc8 100644 --- a/Source/Teensy/DiskImage/d64.h +++ b/Source/Teensy/DiskImage/d64.h @@ -121,6 +121,8 @@ void d64_mem_init(d64_mem_t *m, uint8_t *data, size_t len); int d64_open_mem(d64_t *d, d64_mem_t *m); /* image size selects the format */ int d64_open(d64_t *d, d64_read_fn rd, void *ctx, uint8_t tracks); /* tracks: 35/40 = d64, 70 = d71, 80 = d81 */ int d64_open_size(d64_t *d, d64_read_fn rd, void *ctx, size_t file_size); /* format from the file size (error bytes allowed); -1 if too small */ +int d64_open_exact(d64_t *d, d64_read_fn rd, void *ctx, size_t file_size); /* as d64_open_size, but -1 unless the size is exactly + one kind's, with or without error bytes */ /* Error table: d64_errtab_offset = where it starts in a file of this size (-1: the file has none; after d64_open_size), d64_sectors = * the image's sector count (= the table's length). d64_open_mem sets errtab itself. d64_errcode: an error byte -> DOS code, 0 = no * read error (1 = OK; 2..6, 9, 11, 15 = 20..24, 27, 29, 74; the write errors 25 / 26 / 28 and unknown bytes read fine). */ diff --git a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c index af7253f4..3da6232a 100644 --- a/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c +++ b/Source/Teensy/MinimalBoot/Common/IO_Handlers/TRBlockIO.c @@ -23,12 +23,12 @@ static uint32_t TRBlockBootSize; FLASHMEM static bool TRBlockFileRead(void *Context, uint32_t Offset, uint8_t *Block) { - return TRBlockFile.seekSet(Offset) && TRBlockFile.read(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; + return TRBlockFile.seekSet(Offset) && TRBlockFile.read(Block, D64_SECTOR) == D64_SECTOR; } FLASHMEM static bool TRBlockFileWrite(void *Context, uint32_t Offset, const uint8_t *Block) { - if (!TRBlockFile.seekSet(Offset) || TRBlockFile.write(Block, GEOS_SECTOR_SIZE) != GEOS_SECTOR_SIZE) return false; + if (!TRBlockFile.seekSet(Offset) || TRBlockFile.write(Block, D64_SECTOR) != D64_SECTOR) return false; TRBlockDirty = true; TRBlockLastWritemS = millis(); return true; @@ -55,9 +55,12 @@ FLASHMEM static bool TRBlockOpen() if (!TRBlockFile.open(&SD.sdfs, TRBlockPath, O_RDWR)) return false; Size = TRBlockFile.size(); - TRBlockImg = (struct TRBlockImage){ Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL, true, NULL, - TRBlockFileRead, TRBlockFileWrite, TRBlockFileFlush }; - if (!TRBlockImg.Format) + memset(&TRBlockImg, 0, sizeof TRBlockImg); + TRBlockImg.Writable = true; + TRBlockImg.Read = TRBlockFileRead; + TRBlockImg.Write = TRBlockFileWrite; + TRBlockImg.Flush = TRBlockFileFlush; + if (!TRBlockImageOpen(&TRBlockImg, Size)) { TRBlockFile.close(); return false; diff --git a/Source/Teensy/tests/disk-image.test.js b/Source/Teensy/tests/disk-image.test.js index 9d30795f..78f78a51 100644 --- a/Source/Teensy/tests/disk-image.test.js +++ b/Source/Teensy/tests/disk-image.test.js @@ -63,3 +63,21 @@ test('the image kind follows the file size, error bytes and 40-track D64s includ }; for (const [size, expected] of Object.entries(sizes)) assert.equal(runHarness('size', size), expected, `${size} bytes`); }); + +test('the exact open takes only those sizes, and nothing between them', { skip }, () => { + const sizes = { + 174848: 'd64 35', + 175531: 'd64 35', + 175532: 'none', + 180000: 'none', + 196608: 'd64 40', + 197376: 'd64 40', + 349696: 'd71 70', + 351062: 'd71 70', + 819200: 'd81 80', + 819201: 'none', + 822400: 'd81 80', + 1000000: 'none', + }; + for (const [size, expected] of Object.entries(sizes)) assert.equal(runHarness('exact', size), expected, `${size} bytes`); +}); diff --git a/Source/Teensy/tests/disk_image_harness.c b/Source/Teensy/tests/disk_image_harness.c index f6d3efe2..b05dfcd2 100644 --- a/Source/Teensy/tests/disk_image_harness.c +++ b/Source/Teensy/tests/disk_image_harness.c @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MIT // disk_image_harness new KIND: formats a blank D64, D71 or D81 in memory, saves a 600-byte file on it, reads it // back, and prints "free=, blocks= first=/ same=<0|1>". -// disk_image_harness size BYTES: prints the format and track count d64_open_size picks for a file of that size, -// or "none". +// disk_image_harness size|exact BYTES: prints the format and track count d64_open_size (or d64_open_exact) picks for +// a file of that size, or "none". +#include #include #include @@ -42,12 +43,13 @@ static int NewDisk(const char *Kind) return 0; } -static int SizeOf(const char *Bytes) +static int SizeOf(const char *Bytes, bool Exact) { static const char *const Names[] = { "d64", "d71", "d81" }; + size_t Size = (size_t)strtoul(Bytes, NULL, 10); d64_t Disk; - if (d64_open_size(&Disk, NULL, NULL, (size_t)strtoul(Bytes, NULL, 10))) + if (Exact ? d64_open_exact(&Disk, NULL, NULL, Size) : d64_open_size(&Disk, NULL, NULL, Size)) { printf("none\n"); return 0; @@ -59,6 +61,7 @@ static int SizeOf(const char *Bytes) int main(int argc, char **argv) { if (argc == 3 && !strcmp(argv[1], "new")) return NewDisk(argv[2]); - if (argc == 3 && !strcmp(argv[1], "size")) return SizeOf(argv[2]); + if (argc == 3 && !strcmp(argv[1], "size")) return SizeOf(argv[2], false); + if (argc == 3 && !strcmp(argv[1], "exact")) return SizeOf(argv[2], true); return 2; } diff --git a/Source/Teensy/tests/trblock.test.js b/Source/Teensy/tests/trblock.test.js index e24559d8..5ba4e0e9 100644 --- a/Source/Teensy/tests/trblock.test.js +++ b/Source/Teensy/tests/trblock.test.js @@ -33,6 +33,9 @@ const FORMATS = { d64: { kind: 1, tracks: 35, sectors: zoneSectors }, d71: { kind: 2, tracks: 70, sectors: zoneSectors }, d81: { kind: 3, tracks: 80, sectors: () => 40 }, + 'd64 with 40 tracks': { kind: 1, tracks: 40, sectors: (track) => (track <= 35 ? zoneSectors(track) : 17) }, + 'd64 with error bytes': { kind: 1, tracks: 35, sectors: zoneSectors, errorBytes: true }, + 'd81 with error bytes': { kind: 3, tracks: 80, sectors: () => 40, errorBytes: true }, }; const blockNumber = (format, track, sector) => { @@ -47,8 +50,11 @@ const totalBlocks = (format) => blockNumber(format, format.tracks + 1, 0); const sectorBytes = (number) => Buffer.from(Array.from({ length: SECTOR }, (_, i) => i === 0 ? number & 0xff : i === 1 ? (number >> 8) & 0xff : (i + number) & 0xff)); +// An error table follows the data when the format has one: one byte a sector, 1 meaning no error function makeImage(format) { - return Buffer.concat(Array.from({ length: totalBlocks(format) }, (_, n) => sectorBytes(n))); + const blocks = Array.from({ length: totalBlocks(format) }, (_, n) => sectorBytes(n)); + if (format.errorBytes) blocks.push(Buffer.alloc(totalBlocks(format), 1)); + return Buffer.concat(blocks); } const hex = (n) => n.toString(16); @@ -173,10 +179,14 @@ test('an image that is no known size mounts as no image rather than being guesse const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trblock-odd-')); const image = path.join(dir, 'in.img'), out = path.join(dir, 'out.img'); try { - fs.writeFileSync(image, makeImage(FORMATS.d64).subarray(0, SECTOR * 100)); - for (const cmd of [READ, WRITE, FLUSH, INFO]) { - const ran = spawnSync(harness(), [image, out], { input: command(cmd, { addr0: 1 }) + 'svc\n' + read(STATUS), encoding: 'utf8' }); - assert.equal(ran.stdout.trim().split('\n').at(-1), hex(NO_IMAGE), `cmd ${cmd}`); + const d64 = makeImage(FORMATS.d64), d81 = makeImage(FORMATS['d81 with error bytes']); + for (const odd of [d64.subarray(0, SECTOR * 100), d64.subarray(0, d64.length + 1 - SECTOR), Buffer.concat([d64, Buffer.alloc(1)]), + Buffer.concat([d81, Buffer.alloc(1)])]) { + fs.writeFileSync(image, odd); + for (const cmd of [READ, WRITE, FLUSH, INFO]) { + const ran = spawnSync(harness(), [image, out], { input: command(cmd, { addr0: 1 }) + 'svc\n' + read(STATUS), encoding: 'utf8' }); + assert.equal(ran.stdout.trim().split('\n').at(-1), hex(NO_IMAGE), `${odd.length} bytes, cmd ${cmd}`); + } } } finally { fs.rmSync(dir, { recursive: true, force: true }); diff --git a/Source/Teensy/tests/trblock_harness.c b/Source/Teensy/tests/trblock_harness.c index 7d0ef8ce..c603aab2 100644 --- a/Source/Teensy/tests/trblock_harness.c +++ b/Source/Teensy/tests/trblock_harness.c @@ -11,25 +11,26 @@ #include "../BlockDevice/TRBlockImage.h" -#define MAX_IMAGE 819200 +#define MAX_IMAGE 822400 -static uint8_t Bytes[MAX_IMAGE], Boot[MAX_IMAGE]; +// One byte past the largest image, so an oversized file reads as a size no image has +static uint8_t Bytes[MAX_IMAGE + 1], Boot[MAX_IMAGE]; static uint32_t Size, Flushes; static bool FlushFails; static bool ReadAt(void *Context, uint32_t Offset, uint8_t *Block) { (void)Context; - if (Offset + GEOS_SECTOR_SIZE > Size) return false; - memcpy(Block, Bytes + Offset, GEOS_SECTOR_SIZE); + if (Offset + D64_SECTOR > Size) return false; + memcpy(Block, Bytes + Offset, D64_SECTOR); return true; } static bool WriteAt(void *Context, uint32_t Offset, const uint8_t *Block) { (void)Context; - if (Offset + GEOS_SECTOR_SIZE > Size) return false; - memcpy(Bytes + Offset, Block, GEOS_SECTOR_SIZE); + if (Offset + D64_SECTOR > Size) return false; + memcpy(Bytes + Offset, Block, D64_SECTOR); return true; } @@ -44,7 +45,7 @@ int main(int argc, char **argv) { FILE *In = argc >= 3 ? fopen(argv[1], "rb") : NULL; struct TRBlock Dev; - struct TRBlockImage Image = { NULL, argc < 4 || strcmp(argv[3], "ro"), NULL, ReadAt, WriteAt, FlushAll }; + static struct TRBlockImage Image; struct TRBlockBackend Backend; char Line[64]; @@ -53,9 +54,13 @@ int main(int argc, char **argv) fprintf(stderr, "usage: trblock_harness IMAGE OUT [ro|nobackend|flushfails|boot=FILE]\n"); return 2; } - Size = fread(Bytes, 1, MAX_IMAGE, In); + Size = fread(Bytes, 1, sizeof Bytes, In); fclose(In); - Image.Format = GeosFormatForSize(Size); + Image.Writable = argc < 4 || strcmp(argv[3], "ro"); + Image.Read = ReadAt; + Image.Write = WriteAt; + Image.Flush = FlushAll; + TRBlockImageOpen(&Image, Size); FlushFails = argc >= 4 && !strcmp(argv[3], "flushfails"); Backend = TRBlockImageBackend(&Image); if (argc >= 4 && !strncmp(argv[3], "boot=", 5)) diff --git a/docs/Architecture/Disk-Images.md b/docs/Architecture/Disk-Images.md index 187e47e8..8eddae3e 100644 --- a/docs/Architecture/Disk-Images.md +++ b/docs/Architecture/Disk-Images.md @@ -28,7 +28,17 @@ next row as that row's kind: | 349696, or 351062 with error bytes | D71 | | 819200, or 822400 with error bytes | D81 | +`d64_open_exact()` takes only the sizes in the table and refuses anything between them. The TR block device mounts +through it, so a truncated or padded file is no image rather than one read past its end. + +## Who uses it + +| Caller | What it takes from the engine | +|---|---| +| TR block device (`BlockDevice/TRBlockImage.h`) | `d64_open_exact`, the geometry, `d64_block_read`/`d64_block_write`; `INFO` reports `fmt + 1` and `d64_sectors()`. It leaves the error table unread. | + ## Tests `Source/Teensy/tests/disk-image.test.js` builds the engine with the harness flags as C and as C++, saves and reads a -file on a blank image of each kind, and checks the size table above. +file on a blank image of each kind, and checks the size table above. `trblock.test.js` runs the block device on each +kind, a 40-track D64 and images with error bytes included. diff --git a/docs/TR_Block_Device.md b/docs/TR_Block_Device.md index e29479c5..559ee2d7 100644 --- a/docs/TR_Block_Device.md +++ b/docs/TR_Block_Device.md @@ -19,6 +19,7 @@ This document is the whole contract. The firmware side is [`TRBlock.h`](../Sourc ## What it is and is not * One image at a time, chosen by the firmware (for GEOS, when the menu launches it). The C64 cannot choose or replace the image. * Blocks are 256 bytes. A block is addressed by two bytes whose meaning depends on the image kind ([Disk addresses](#disk-addresses)). +* The image is a D64 (35 or 40 tracks), D71 or D81, with or without the error bytes some copiers append. A file of any other size does not mount. The error bytes are not read: a sector they mark bad reads as its bytes. * It is available while the GeoRAM handler or the `REU+USB Mouse/Disk` handler (TR+ only) is active, which is also where the USB mouse registers are. It is not available under other handlers, the plain REU included, and not under a Host Extension. * It is not an IEC drive. Software that talks to the IEC bus cannot see it. @@ -101,7 +102,7 @@ Any other CMD fails with `$E6`. A failed `READ` or `BOOT` leaves the buffer unspecified. A failed `WRITE` that was rejected before the card was touched (`$E1`, `$E2`, `$E5`) changes nothing; one the card failed (`$E4`) may have changed part of the block. ## Disk addresses -For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector, as the 1541, 1571 and 1581 number them: tracks start at 1, sectors at 0. The 1541 and 1571 use the usual four speed zones (21, 19, 18 and 17 sectors a track, the 1571's second side repeating them); the 1581 has 40 sectors on every track. An address off the end of the image or past a track's last sector fails with `$E2`. +For the disk image kinds (D64, D71, D81) ADDR0 is the track and ADDR1 the sector, as the 1541, 1571 and 1581 number them: tracks start at 1, sectors at 0. The 1541 and 1571 use the usual four speed zones (21, 19, 18 and 17 sectors a track, the 1571's second side repeating them, a 40-track D64's tracks 36 to 40 having 17); the 1581 has 40 sectors on every track. The arithmetic is the shared disk image engine's ([Disk-Images.md](Architecture/Disk-Images.md)). An address off the end of the image or past a track's last sector fails with `$E2`. The firmware does this arithmetic so that a 6502 driver does not have to. A driver that wants raw sector numbers instead is a different image kind, which this version does not define.