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/D64.ino b/Source/Teensy/D64.ino index a94c7b92..e0376166 100644 --- a/Source/Teensy/D64.ino +++ b/Source/Teensy/D64.ino @@ -17,6 +17,8 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "DiskImage/MenuDisk.h" + enum enDxxFileNameBytes { DxxFNB_NameLength = 16, @@ -36,25 +38,25 @@ enum enDxxFileTypes DxxFT_REL = 4, }; -FLASHMEM uint32_t D64Offset(uint8_t Track, uint8_t Sector) +// An image file open for the shared disk image engine (DiskImage/MenuDisk.h) +struct DxxImage +{ + File *Image; + d64_t Disk; +}; + +FLASHMEM static int DxxSectorRead(void *Context, uint8_t Track, uint8_t Sector, uint8_t *Block) { - if (Track<18) return ( ((Track- 1)*21+Sector)*256); - if (Track<25) return (0x16500+((Track-18)*19+Sector)*256); - if (Track<31) return (0x1ea00+((Track-25)*18+Sector)*256); - return (0x25600+((Track-31)*17+Sector)*256); + struct DxxImage *Dxx = (struct DxxImage *)Context; + return Dxx->Image->seek(d64_sector_offset(&Dxx->Disk, Track, Sector)) && Dxx->Image->read(Block, D64_SECTOR) == D64_SECTOR ? 0 : -1; } -FLASHMEM uint32_t DxxOffset(uint8_t DiskType, uint8_t Track, uint8_t Sector) +FLASHMEM static bool DxxOpen(struct DxxImage *Dxx, File *Image, uint8_t DiskType) { - if (DiskType == rtD64) return D64Offset(Track, Sector); + enum MenuDiskKind Kind = DiskType == rtD71 ? MenuDiskD71 : DiskType == rtD81 ? MenuDiskD81 : MenuDiskD64; - if (DiskType == rtD71) - { - if (Track>35) return (0x2AB00 + D64Offset(Track-35, Sector)); //side 1 - else return D64Offset(Track, Sector); //side 0 - } - - return (((Track- 1)*40+Sector)*256); // Defaulte to (DiskType == rtD81) + Dxx->Image = Image; + return MenuDiskOpen(&Dxx->Disk, Kind, Image->size(), DxxSectorRead, Dxx); } FLASHMEM uint8_t ItemTypeFromDxxFileType(uint8_t FileType) @@ -74,6 +76,30 @@ FLASHMEM uint8_t ItemTypeFromDxxFileType(uint8_t FileType) return RetItemType; } +FLASHMEM static bool AddDxxEntry(void *Context, const uint8_t *Name, uint8_t FileType, uint8_t FileTrack, uint8_t FileSect) +{ + if (NumDrvDirMenuItems >= MaxMenuItems) return false; + + char *Entry = (char*)malloc(DxxFNB_Bytes); // 16 char max + term + ftrack + fsec + DiskType + for(uint8_t CharNum=0; CharNumopen(DriveDirPath, FILE_READ); //path includes d64/71/81 file + struct DxxImage Dxx; if (!myFile) return; - uint32_t FileSize = myFile.size(); - Printf_dbg("Loading dir from %s (%lu bytes) type: %d\n", DriveDirPath, FileSize, DiskType); - if((DiskType == rtD64 && (FileSize < 171520 || FileSize > 197376)) || // Realm.d64 = 171520 - (DiskType == rtD71 && (FileSize < 349696 || FileSize > 351062)) || - (DiskType == rtD81 && (FileSize < 819200 || FileSize > 822400)) ) + Printf_dbg("Loading dir from %s (%lu bytes) type: %d\n", DriveDirPath, (uint32_t)myFile.size(), DiskType); + if (!DxxOpen(&Dxx, &myFile, DiskType)) { //Create dummy file/name to display err message DriveDirMenu[1].ItemType = rtUnknown; @@ -101,76 +125,7 @@ FLASHMEM void LoadDxxDirectory(FS *sourceFS, uint8_t DiskType) return; } - uint8_t Track = 18; - uint8_t Sector = 1; - uint8_t SecOffset = 0; - uint8_t FileName[16]; - uint8_t NextTrack, NextSect, FileType, FileTrack, FileSect; - - if(DiskType == rtD81) - { - Track = 40; - Sector = 3; - } - - //An image's directory chain can link back to itself; a real one is at most 37 sectors (D81) - const uint8_t MaxDxxDirSectors = 64; - uint8_t DirSectorsRead = 0; - while(Track != 0 && DirSectorsRead++ < MaxDxxDirSectors && NumDrvDirMenuItems < MaxMenuItems) - { - uint32_t CurTSOffset = DxxOffset(DiskType, Track, Sector); - Printf_dbg("Track:%d Sector:%d = DxxOffset:$%x\n", Track, Sector, CurTSOffset); - - do - { - myFile.seek(CurTSOffset+SecOffset); - - NextTrack = myFile.read(); //0x00 Only first is valid, rest in sect should be 00 - NextSect = myFile.read(); //0x01 Only first is valid, rest in sect should be 00 - FileType = myFile.read(); //0x02 - FileTrack = myFile.read(); //0x03 - FileSect = myFile.read(); //0x04 - myFile.read(FileName, DxxFNB_NameLength); //0x05-0x14, padded with $a0 (space) - - Printf_dbg(" SecOffset $%02x: NTrack:%02d NSect:%02d Type:$%02x FTrack:%02d FSect:%02d\n", - SecOffset, NextTrack, NextSect, FileType, FileTrack, FileSect); - - if(SecOffset == 0) - { - Track = NextTrack; //will be 0 if reading last sector - Sector = NextSect; - } - - if (FileName[0] && NumDrvDirMenuItems >= MaxMenuItems) SecOffset = 0; //array full: stop - else if (FileName[0]) //check for end of dir, no entry - { //valid dir entry - DriveDirMenu[NumDrvDirMenuItems].Name = (char*)malloc(DxxFNB_Bytes); // 16 char max + term + ftrack + fsec + DiskType - - for(uint8_t CharNum=0; CharNumName[DxxFNB_Track]; - uint8_t Sector = MyMenuItem->Name[DxxFNB_Sector]; - uint8_t DiskType = MyMenuItem->Name[DxxFNB_DiskType]; - MyMenuItem->Size = 0; + struct DxxImage Dxx; + int32_t Length = DxxOpen(&Dxx, &myFile, MyMenuItem->Name[DxxFNB_DiskType]) + ? MenuDiskReadFile(&Dxx.Disk, MyMenuItem->Name[DxxFNB_Track], MyMenuItem->Name[DxxFNB_Sector], RAM_Image, RAM_ImageSize) + : MENUDISK_BROKEN; - // Load the whole thing into the RAM1 buffer - while(Track != 0) + if (Length < 0) { - uint32_t CurTSOffset = DxxOffset(DiskType, Track, Sector); - Printf_dbg("Track:%d Sector:%d = D64Offset:$%06x\n", Track, Sector, CurTSOffset); - - if (MyMenuItem->Size + 254 > RAM_ImageSize) - { - SendMsgPrintfln("File too large"); - myFile.close(); - return false; - } - myFile.seek(CurTSOffset); - Track = myFile.read(); //0x00 - Sector = myFile.read(); //0x01 - - uint16_t BytesToCopy = 254; //normal sector: full 254 data bytes used - if (Track == 0) - { //last sector: Sector byte instead gives the offset of the last used byte - //(counting the 2 link bytes), so valid data bytes = Sector-1 - BytesToCopy = (Sector >= 1) ? (Sector - 1) : 0; - if (BytesToCopy > 254) BytesToCopy = 254; //safety clamp for corrupt images - } - - myFile.read(RAM_Image+MyMenuItem->Size, BytesToCopy); //read in only the valid bytes - MyMenuItem->Size += BytesToCopy; + if (Length == MENUDISK_TOO_BIG) SendMsgPrintfln(PSTR("File too large")); + else SendMsgPrintfln(PSTR("File could not be read")); + myFile.close(); + return false; } - + MyMenuItem->Size = Length; + SendMsgPrintfln("Done"); myFile.close(); Printf_dbg("Size:$%04x\n", MyMenuItem->Size); diff --git a/Source/Teensy/DiskImage/DiskImage.h b/Source/Teensy/DiskImage/DiskImage.h new file mode 100644 index 00000000..92efbf62 --- /dev/null +++ b/Source/Teensy/DiskImage/DiskImage.h @@ -0,0 +1,19 @@ +// 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 +#else +#define FLASHMEM +#endif + +#include "d64.h" +#include "d64.c" diff --git a/Source/Teensy/DiskImage/MenuDisk.h b/Source/Teensy/DiskImage/MenuDisk.h new file mode 100644 index 00000000..c1814000 --- /dev/null +++ b/Source/Teensy/DiskImage/MenuDisk.h @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +// The TR menu's view of a D64, D71 or D81 (D64.ino): browsing an image and loading a file from it, read only, +// through the shared engine. The menu picks the kind from the file's extension. +#pragma once +#include + +#include "DiskImage.h" + +enum MenuDiskKind { MenuDiskD64, MenuDiskD71, MenuDiskD81 }; + +#define MENUDISK_BROKEN (-1) // a link off the image, a read that failed, or a chain that loops +#define MENUDISK_TOO_BIG (-2) + +// A file of the kind's size, with or without error bytes, or a short image some copiers write (Realm.d64 is +// 171520 bytes): a sector past its end fails to read rather than the image being refused +FLASHMEM static bool MenuDiskOpen(d64_t *Disk, enum MenuDiskKind Kind, uint64_t Size, d64_read_fn Read, void *Context) +{ + static const uint32_t Smallest[] D64_FLASH = { 171520, 349696, 819200 }; + static const uint32_t Largest[] D64_FLASH = { 197376, 351062, 822400 }; + static const uint8_t Tracks[] D64_FLASH = { 35, 70, 80 }; + + if (Size < Smallest[Kind] || Size > Largest[Kind]) return false; + d64_open(Disk, Read, Context, Kind == MenuDiskD64 && Size >= 196608 ? 40 : Tracks[Kind]); + return true; +} + +// The bytes of the file whose first sector is Track/Sector: its length, MENUDISK_BROKEN or MENUDISK_TOO_BIG +FLASHMEM static int32_t MenuDiskReadFile(d64_t *Disk, uint8_t Track, uint8_t Sector, uint8_t *Out, uint32_t Capacity) +{ + d64_entry_t Entry; + d64_file_t File; + int Read; + + memset(&Entry, 0, sizeof Entry); + Entry.track = Track; + Entry.sector = Sector; + d64_file_open(Disk, &Entry, &File); + Read = d64_file_read(Disk, &File, Out, Capacity); + if (Read < 0) return MENUDISK_BROKEN; + if (!d64_file_eof(&File)) return MENUDISK_TOO_BIG; + return Read; +} + +// Called for each file the menu lists: its 16 name bytes as stored (PETSCII, $A0 padded), its type byte and first +// sector. Returns false when the menu has no room for more. +typedef bool (*MenuDiskAddFn)(void *Context, const uint8_t *Name, uint8_t Type, uint8_t Track, uint8_t Sector); + +// The files in the directory, as a drive's listing shows them: a deleted entry (type byte 0) is left out, and an +// empty slot does not end its sector. The listing starts at the first directory sector (18/1, or 40/3 on a D81) +// rather than at the header's link, as the menu always has. Returns 0, or MENUDISK_BROKEN when the chain broke +// partway (what was listed before stays). +FLASHMEM static int MenuDiskList(d64_t *Disk, MenuDiskAddFn Add, void *Context) +{ + d64_dir_t Dir; + d64_entry_t Entry; + int Result; + + if (d64_dir_first(Disk, &Dir)) return MENUDISK_BROKEN; + Dir.track = Disk->fmt == D64_FMT_D81 ? 40 : 18; + Dir.sector = Disk->fmt == D64_FMT_D81 ? 3 : 1; + while ((Result = d64_dir_next(Disk, &Dir, &Entry)) > 0) + { + if (!Add(Context, Dir.buf + Entry.dir_slot * 32 + 5, Entry.type, Entry.track, Entry.sector)) return 0; + } + return Result ? MENUDISK_BROKEN : 0; +} diff --git a/Source/Teensy/DiskImage/d64.c b/Source/Teensy/DiskImage/d64.c new file mode 100644 index 00000000..645f5ffb --- /dev/null +++ b/Source/Teensy/DiskImage/d64.c @@ -0,0 +1,1246 @@ +/* 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_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; + 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 < dir_sector0(d)) continue; /* the header, and on a D81 its BAM sectors */ + 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_alloc_next(d64_t *d, uint8_t *t, uint8_t *s) +{ + if (!d->write || d->ro) return D64_E_RO; + return alloc_sector(d, t, s, 10, 0); +} + +VD_COLD static int popcount_track(const d64_t *d, uint8_t t, const uint8_t *bits) +{ + int n = 0; + for (int k = 0; k < spt(d, t); k++) n += (bits[k / 8] >> (k % 8)) & 1; + return n; +} + +VD_COLD int d64_blocks_allocatable(d64_t *d) +{ + uint8_t bufA[D64_SECTOR], bufB[D64_SECTOR], *cnt, *bits; + bamloc_t L; + int n = 0; + for (uint8_t t = 1; t <= bam_tracks(d); t++) { + if (bam_reserved(d, t)) continue; + int r = bam_load(d, t, &L, bufA, bufB, &cnt, &bits); + if (r) return r; + int f = popcount_track(d, t, bits); + n += *cnt < f ? *cnt : f; /* alloc_sector needs both a count above 0 and a free bit */ + } + return n; +} + +VD_COLD int d64_dir_check(d64_t *d, uint8_t *t, uint8_t *s) +{ + uint8_t b[D64_SECTOR], seen[D64_SECTOR / 8], dt = dir_track(d); + int r = rd(d, dt, 0, b); + if (r) return r; + memset(seen, 0, sizeof seen); + *t = b[0]; *s = b[1]; + if (!*t) return D64_E_NOTFOUND; + while (*t) { + if (*t != dt || *s < dir_sector0(d) || *s >= spt(d, dt) || (seen[*s / 8] >> (*s % 8)) & 1) return D64_E_LOOP; + seen[*s / 8] |= (uint8_t)(1 << (*s % 8)); + if ((r = rd(d, *t, *s, b))) return r; + *t = b[0]; *s = b[1]; + } + return 0; +} + +VD_COLD int d64_dir_room(d64_t *d, uint8_t *t, uint8_t *s) +{ + uint8_t b[D64_SECTOR], bufA[D64_SECTOR], bufB[D64_SECTOR], *cnt, *bits, dt = dir_track(d); + bamloc_t L; + int r = d64_dir_check(d, t, s); + if (r) return r; + if ((r = rd(d, dt, 0, b))) return r; + for (*t = b[0], *s = b[1]; *t; *t = b[0], *s = b[1]) { + if ((r = rd(d, *t, *s, b))) return r; + for (int i = 0; i < 8; i++) if (b[i * 32 + 2] == 0) return 1; + } + if ((r = bam_load(d, dt, &L, bufA, bufB, &cnt, &bits))) return r; + if ((r = rd(d, dt, 0, b))) return r; + for (*t = b[0], *s = b[1]; *t; *t = b[0], *s = b[1]) { /* a sector growing the directory must not be one it holds */ + if (bits[*s / 8] & (1 << (*s % 8))) return D64_E_BAM; + if ((r = rd(d, *t, *s, b))) return r; + } + if (*cnt > 0) + for (int k = dir_sector0(d); k < spt(d, dt); k++) if (bits[k / 8] & (1 << (k % 8))) return 0; + return D64_E_FULL; +} + +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..92f29f27 --- /dev/null +++ b/Source/Teensy/DiskImage/d64.h @@ -0,0 +1,231 @@ +/* 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) */ + D64_E_BAM = -12 /* the BAM shows a sector free that the directory holds */ +}; + +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 */ +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). */ +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 */ +/* The next data sector as a file write takes it: after *t, *s with the file interleave, or (*t == 0) the first in DOS order. */ +int d64_alloc_next(d64_t *d, uint8_t *t, uint8_t *s); + +/* Checks before a write, so a refusal comes before anything changes: + * d64_blocks_allocatable: data sectors the allocator can still hand out (per track the smaller of its BAM count and its + * free bits), which is d64_blocks_free when the BAM agrees with itself + * d64_dir_check: 0 when the header links to a chain of distinct directory sectors on the directory track; + * D64_E_NOTFOUND when it links nowhere, D64_E_LOOP with *t, *s the first link that breaks the chain + * d64_dir_room: 1 when the directory has a free slot, 0 when it can take one more sector, D64_E_FULL when it cannot, + * D64_E_BAM with *t, *s a directory sector the BAM shows free (it could be handed out again); d64_dir_check's codes first */ +int d64_blocks_allocatable(d64_t *d); +int d64_dir_check(d64_t *d, uint8_t *t, uint8_t *s); +int d64_dir_room(d64_t *d, uint8_t *t, uint8_t *s); + +/* 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/GEOS/GeosBoot.h b/Source/Teensy/GEOS/GeosBoot.h index 5450c1a2..68a6bb9b 100644 --- a/Source/Teensy/GEOS/GeosBoot.h +++ b/Source/Teensy/GEOS/GeosBoot.h @@ -20,7 +20,6 @@ #define GEOS_DRIVER_RECORD 4 #define GEOS_BASIC_START 0x0801 #define GEOS_MEMORY_END 0x10000 -#define GEOS_MAX_FILE_BLOCKS 3200 #define GEOS_DEVICE_8 8 #define GEOS128_SHARED_RAM_END 0x2000 // the C128 boot ROM's loader runs in the RAM below, which both banks share #define GEOS128_IO_START 0xd000 // and writes bank 1 with I/O mapped in from here up @@ -136,46 +135,41 @@ FLASHMEM static uint8_t *GeosFindOnce(uint8_t *Body, uint32_t Length, const uint FLASHMEM static uint32_t GeosBytesInBlock(const uint8_t *Block) { return Block[0] ? GEOS_DATA_PER_BLOCK : (Block[1] > 1 ? Block[1] - 1 : 0); } -// A file's bytes along its sector chain from At; What names it in a refusal -FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, struct GeosTS At, const char *What, uint8_t *Out, +// A file's bytes along its sector chain from Track/Sector; What names it in a refusal +FLASHMEM static bool GeosReadChain(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const char *What, uint8_t *Out, uint32_t Capacity, uint32_t *Length) { - uint8_t Block[GEOS_SECTOR_SIZE]; - - *Length = 0; - for (int Blocks = 0; At.Track; Blocks++) + d64_entry_t Start = { 0 }; + d64_file_t File; + int Read; + + Start.track = Track; + Start.sector = Sector; + d64_file_open(&Disk->Image, &Start, &File); + Read = d64_file_read(&Disk->Image, &File, Out, Capacity); + if (Read < 0 && !Disk->Error[0]) snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s's chain is broken"), What); + if (Read < 0) return false; + if (!d64_file_eof(&File)) { - uint32_t Used; - if (Blocks > GEOS_MAX_FILE_BLOCKS || At.Track > Disk->Format->Tracks) - { - snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s's chain is broken"), What); - return false; - } - if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; - Used = GeosBytesInBlock(Block); - if (*Length + Used > Capacity) - { - snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is too large"), What); - return false; - } - memcpy(Out + *Length, Block + 2, Used); - *Length += Used; - At = (struct GeosTS){ Block[0], Block[1] }; + snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is too large"), What); + return false; } + *Length = (uint32_t)Read; return true; } // Writes Data back over the chain GeosReadChain read it from -FLASHMEM static bool GeosRewriteChain(struct GeosDisk *Disk, struct GeosTS At, const uint8_t *Data) +FLASHMEM static bool GeosRewriteChain(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Data) { uint8_t Block[GEOS_SECTOR_SIZE]; - for (uint32_t Done = 0; At.Track; Done += GEOS_DATA_PER_BLOCK) + for (uint32_t Done = 0; Track; Done += GEOS_DATA_PER_BLOCK) { - if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; + if (d64_block_read(&Disk->Image, Track, Sector, Block)) return GeosFailed(Disk, PSTR("the image could not be read")); memcpy(Block + 2, Data + Done, GeosBytesInBlock(Block)); - if (!GeosWriteTS(Disk, At.Track, At.Sector, Block)) return false; - At = (struct GeosTS){ Block[0], Block[1] }; + if (d64_block_write(&Disk->Image, Track, Sector, Block)) return GeosFailed(Disk, PSTR("the image could not be written")); + Track = Block[0]; + Sector = Block[1]; } return true; } @@ -183,25 +177,14 @@ FLASHMEM static bool GeosRewriteChain(struct GeosDisk *Disk, struct GeosTS At, c // A file's bytes, found by name in the directory and followed along its sector chain FLASHMEM static bool GeosReadFile(struct GeosDisk *Disk, const char *Name, uint8_t *Out, uint32_t Capacity, uint32_t *Length) { - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS], At = { 0, 0 }; - uint8_t Block[GEOS_SECTOR_SIZE]; - struct GeosDirScan Scan; - int Count; - - if (!GeosReadDirectory(Disk, Chain, &Count, &Scan)) return false; - for (int i = 0; i < Count && !At.Track; i++) - { - if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; - for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) - { - const uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; - if (!(Slot[SlotType] & CBM_TYPE_MASK) || !GeosSlotNamed(Slot, Name)) continue; - At = (struct GeosTS){ Slot[SlotFirst], Slot[SlotFirst + 1] }; - break; - } - } - if (!At.Track) return GeosFail(Disk, PSTR("the boot file is not on the disk")); - return GeosReadChain(Disk, At, PSTR("the boot file"), Out, Capacity, Length); + d64_entry_t Entry; + int Found; + + if (!GeosCheckDisk(Disk)) return false; + Found = d64_find(&Disk->Image, Name, &Entry); + if (Found < 0) return GeosFailed(Disk, PSTR("the image could not be read")); + if (!Found) return GeosFail(Disk, PSTR("the boot file is not on the disk")); + return GeosReadChain(Disk, Entry.track, Entry.sector, PSTR("the boot file"), Out, Capacity, Length); } static bool GeosNameContains(const uint8_t *Name, const char *Part) @@ -215,37 +198,39 @@ static bool GeosNameContains(const uint8_t *Name, const char *Part) return false; } -// Whether Slot is the target's stock CONFIGURE, set up for the TR now or before: its probe answers what the TR has +// Whether Entry is the target's stock CONFIGURE, set up for the TR now or before: its probe answers what the TR has // and the 1581 driver it installs for drive A is the TR's. The driver goes first, so a write cut short leaves the // stock probe, and next time the driver no longer looks stock and the file is put aside instead. -FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const uint8_t *Slot, uint8_t *Scratch, uint32_t Capacity, - bool *Ours, bool *Changed) +FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const d64_entry_t *Entry, const uint8_t *Slot, uint8_t *Scratch, + uint32_t Capacity, bool *Ours, bool *Changed) { const char *Name = Disk->Target->ConfigureName; uint8_t Index[GEOS_SECTOR_SIZE], *Driver, *At; - struct GeosTS Probe, Driven; + uint8_t ProbeTrack, ProbeSector, DrivenTrack, DrivenSector; uint32_t ProbeLength, DriverLength; uint16_t ReadBlock; *Ours = false; - if (Slot[SlotStructure] != GEOS_VLIR || !GeosSlotNamed(Slot, Name)) return true; - if (!GeosReadTS(Disk, Slot[SlotFirst], Slot[SlotFirst + 1], Index)) return false; - Probe = (struct GeosTS){ Index[2 + 2 * GEOS_PROBE_RECORD], Index[3 + 2 * GEOS_PROBE_RECORD] }; - Driven = (struct GeosTS){ Index[2 + 2 * GEOS_DRIVER_RECORD], Index[3 + 2 * GEOS_DRIVER_RECORD] }; - if (!Probe.Track || !Driven.Track) return true; - if (!GeosReadChain(Disk, Probe, Name, Scratch, Capacity, &ProbeLength)) return false; + if (Slot[SlotStructure] != GEOS_VLIR || strcmp(Entry->name, Name)) return true; + if (d64_block_read(&Disk->Image, Entry->track, Entry->sector, Index)) return GeosFailed(Disk, PSTR("the image could not be read")); + ProbeTrack = Index[2 + 2 * GEOS_PROBE_RECORD]; + ProbeSector = Index[3 + 2 * GEOS_PROBE_RECORD]; + DrivenTrack = Index[2 + 2 * GEOS_DRIVER_RECORD]; + DrivenSector = Index[3 + 2 * GEOS_DRIVER_RECORD]; + if (!ProbeTrack || !DrivenTrack) return true; + if (!GeosReadChain(Disk, ProbeTrack, ProbeSector, Name, Scratch, Capacity, &ProbeLength)) return false; if (GeosFindOnce(Scratch, ProbeLength, GeosTRProbe, sizeof GeosTRProbe)) return *Ours = true; At = GeosFindOnce(Scratch, ProbeLength, GeosStockProbe, sizeof GeosStockProbe); if (!At || At + sizeof GeosTRProbe > Scratch + ProbeLength) return true; Driver = Scratch + ProbeLength; - if (!GeosReadChain(Disk, Driven, Name, Driver, Capacity - ProbeLength, &DriverLength)) return false; + if (!GeosReadChain(Disk, DrivenTrack, DrivenSector, Name, Driver, Capacity - ProbeLength, &DriverLength)) return false; if (DriverLength <= GEOS_TYPE_BYTE || !GeosIsDriverAt(Driver)) return true; ReadBlock = GeosWord(Driver + 2 * VecReadBlock); if (ReadBlock < GEOS_DRIVER_AREA || ReadBlock > GEOS_DRIVER_AREA + DriverLength) return true; if (!GeosReplaceTransport(Driver, Disk->Target->DiskDriver)) return true; memcpy(At, GeosTRProbe, sizeof GeosTRProbe); - if (!GeosRewriteChain(Disk, Driven, Driver) || !GeosRewriteChain(Disk, Probe, Scratch)) return false; + if (!GeosRewriteChain(Disk, DrivenTrack, DrivenSector, Driver) || !GeosRewriteChain(Disk, ProbeTrack, ProbeSector, Scratch)) return false; *Changed = true; return *Ours = true; } @@ -255,35 +240,28 @@ FLASHMEM static bool GeosSetUpConfigure(struct GeosDisk *Disk, const uint8_t *Sl // Returns whether anything changed. static inline bool GeosPrepareConfigure(struct GeosDisk *Disk, uint8_t *Scratch, uint32_t Capacity, bool *Changed) { - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; - uint8_t Block[GEOS_SECTOR_SIZE]; - struct GeosDirScan Scan; - int Count; + d64_dir_t Dir; + d64_entry_t Entry; + int Result; *Changed = false; - if (!GeosReadDirectory(Disk, Chain, &Count, &Scan)) return false; - for (int i = 0; i < Count; i++) + if (!GeosCheckDisk(Disk)) return false; + if (d64_dir_first(&Disk->Image, &Dir)) return GeosFailed(Disk, PSTR("the image could not be read")); + while ((Result = d64_dir_next(&Disk->Image, &Dir, &Entry)) > 0) { - bool Retyped = false; - if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; - for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) - { - uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE, Type = Slot[SlotGeosType]; - bool Ours; - if (!(Slot[SlotType] & CBM_TYPE_MASK)) continue; - if (Type != GEOS_AUTO_EXEC && Type != GEOS_SYSTEM_FILE) continue; - if (!GeosNameContains(Slot + SlotName, GEOS_CONFIG_NAME)) continue; - if (!GeosSetUpConfigure(Disk, Slot, Scratch, Capacity, &Ours, Changed)) return false; - Type = Ours ? GEOS_AUTO_EXEC : GEOS_SYSTEM_FILE; - if (Slot[SlotGeosType] == Type) continue; - Slot[SlotGeosType] = Type; - Retyped = true; - } - if (!Retyped) continue; - if (!GeosWriteTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; + uint8_t *Slot = Dir.buf + Entry.dir_slot * GEOS_DIR_ENTRY_SIZE, Type = Slot[SlotGeosType]; + bool Ours; + if (!(Slot[SlotType] & CBM_TYPE_MASK)) continue; + if (Type != GEOS_AUTO_EXEC && Type != GEOS_SYSTEM_FILE) continue; + if (!GeosNameContains(Slot + SlotName, GEOS_CONFIG_NAME)) continue; + if (!GeosSetUpConfigure(Disk, &Entry, Slot, Scratch, Capacity, &Ours, Changed)) return false; + Type = Ours ? GEOS_AUTO_EXEC : GEOS_SYSTEM_FILE; + if (Slot[SlotGeosType] == Type) continue; + Slot[SlotGeosType] = Type; + if (d64_block_write(&Disk->Image, Entry.dir_t, Entry.dir_s, Dir.buf)) return GeosFailed(Disk, PSTR("the image could not be written")); *Changed = true; } - return true; + return Result == 0 || GeosFailed(Disk, PSTR("the image could not be read")); } // BASIC "10 SYS 2061" and the stub it runs: the screen colors and drive 8 the stock GEOS start-up file sets, then diff --git a/Source/Teensy/GEOS/GeosDiskPatch.h b/Source/Teensy/GEOS/GeosDiskPatch.h index bd840f35..552f3b03 100644 --- a/Source/Teensy/GEOS/GeosDiskPatch.h +++ b/Source/Teensy/GEOS/GeosDiskPatch.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT -// Puts a TR input driver on a GEOS boot disk image, block by block. -// Mirrors tools/geos/trmouse.py install(), which the host test holds it to byte for byte. +// Puts a TR input driver on a GEOS boot disk image, through the shared disk image engine +// (docs/Architecture/Disk-Images.md). Mirrors tools/geos/trmouse.py install(), which the host test +// holds it to byte for byte. #pragma once #include #include @@ -12,18 +13,16 @@ #define FLASHMEM #define PSTR(s) (s) #endif +#include "../DiskImage/DiskImage.h" -#define GEOS_SECTOR_SIZE 256 +#define GEOS_SECTOR_SIZE D64_SECTOR #define GEOS_DIR_ENTRY_SIZE 32 -#define GEOS_ENTRIES_PER_DIR (GEOS_SECTOR_SIZE / GEOS_DIR_ENTRY_SIZE) #define GEOS_NAME_SIZE 16 -#define GEOS_MAX_DIR_SECTORS 40 #define GEOS_ERROR_SIZE 80 #define GEOS_DATA_PER_BLOCK (GEOS_SECTOR_SIZE - 2) -#define CBM_NAME_PAD 0xa0 #define CBM_TYPE_MASK 0x07 -#define CBM_USR_CLOSED 0x83 +#define CBM_USR 3 #define GEOS_SEQUENTIAL 0 #define D71_DOUBLE_SIDED 0x80 @@ -40,19 +39,6 @@ enum GeosSlot SlotBlocks = 30, }; -struct GeosPlace { uint8_t Track, Sector, Byte; }; -struct GeosTS { uint8_t Track, Sector; }; - -struct GeosFormat -{ - const char *Name; - uint8_t Tracks, DirTrack, FirstDirSector, LastDirSector; - uint8_t Reserved[2]; - uint32_t Size; - bool FixedSectors; // D81: 40 per track; else the 1541's zones, repeated on a 1571's second side - bool TwoSided; -}; - struct GeosDate { uint8_t Year, Month, Day, Hour, Minute; }; // A GEOS version the TR sets up: its boot disk's DeskTop and the input driver the TR adds @@ -73,10 +59,13 @@ struct GeosTarget struct GeosBootDisk { bool HasDeskTop, HasDriver; }; -// A boot disk image reached through 256-byte block reads and writes at byte offsets. +// A boot disk image reached through 256-byte block reads and writes at byte offsets. Set Target, Context +// and the callbacks (Write NULL for a read-only image), then GeosDiskOpen. The engine keeps a pointer to +// the struct, so it stays where it was opened. struct GeosDisk { - const struct GeosFormat *Format; + d64_t Image; + bool Open; const struct GeosTarget *Target; void *Context; bool (*Read)(void *Context, uint32_t Offset, uint8_t *Block); @@ -84,247 +73,93 @@ struct GeosDisk char Error[GEOS_ERROR_SIZE]; }; -static const struct GeosFormat GeosFormats[] = -{ - { "D64", 35, 18, 1, 18, { 18, 0 }, 174848, false, false }, - { "D71", 70, 18, 1, 18, { 18, 53 }, 349696, false, true }, - { "D81", 80, 40, 3, 39, { 40, 0 }, 819200, true, false }, -}; - FLASHMEM static bool GeosFail(struct GeosDisk *Disk, const char *Message) { snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s"), Message); return false; } -FLASHMEM static uint8_t GeosSectorsOnTrack(const struct GeosFormat *Format, uint8_t Track) +// For an engine call that failed: a card that failed has said so already, which is the better reason +FLASHMEM static bool GeosFailed(struct GeosDisk *Disk, const char *Message) { - uint8_t ZoneTrack = (Track - 1) % 35 + 1; - - if (Format->FixedSectors) return 40; - return ZoneTrack <= 17 ? 21 : ZoneTrack <= 24 ? 19 : ZoneTrack <= 30 ? 18 : 17; + if (!Disk->Error[0]) GeosFail(Disk, Message); + return false; } -FLASHMEM static uint32_t GeosOffset(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +FLASHMEM static int GeosSectorRead(void *Context, uint8_t Track, uint8_t Sector, uint8_t *Block) { - uint32_t Blocks = Sector; + struct GeosDisk *Disk = (struct GeosDisk *)Context; - for (uint8_t t = 1; t < Track; t++) Blocks += GeosSectorsOnTrack(Format, t); - return Blocks * GEOS_SECTOR_SIZE; + if (Disk->Read(Disk->Context, (uint32_t)d64_sector_offset(&Disk->Image, Track, Sector), Block)) return 0; + GeosFail(Disk, PSTR("the image could not be read")); + return -1; } -FLASHMEM static bool GeosReadTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, uint8_t *Block) +FLASHMEM static int GeosSectorWrite(void *Context, uint8_t Track, uint8_t Sector, const uint8_t *Block) { - if (Disk->Read(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; - return GeosFail(Disk, PSTR("the image could not be read")); -} + struct GeosDisk *Disk = (struct GeosDisk *)Context; -FLASHMEM static bool GeosWriteTS(struct GeosDisk *Disk, uint8_t Track, uint8_t Sector, const uint8_t *Block) -{ - if (Disk->Write(Disk->Context, GeosOffset(Disk->Format, Track, Sector), Block)) return true; - return GeosFail(Disk, PSTR("the image could not be written")); + if (Disk->Write(Disk->Context, (uint32_t)d64_sector_offset(&Disk->Image, Track, Sector), Block)) return 0; + GeosFail(Disk, PSTR("the image could not be written")); + return -1; } -FLASHMEM static bool GeosIsReserved(const struct GeosFormat *Format, uint8_t Track) +FLASHMEM static bool GeosDiskOpen(struct GeosDisk *Disk, uint64_t Size) { - return Track == Format->Reserved[0] || Track == Format->Reserved[1]; + Disk->Error[0] = 0; + Disk->Open = Size <= SIZE_MAX && !d64_open_exact(&Disk->Image, GeosSectorRead, Disk, (size_t)Size); + if (Disk->Open && Disk->Write) Disk->Image.write = GeosSectorWrite; + return Disk->Open; } -FLASHMEM static bool GeosIsDirSector(const struct GeosFormat *Format, uint8_t Track, uint8_t Sector) +FLASHMEM static uint8_t GeosDirTrack(const struct GeosDisk *Disk) { - return Track == Format->DirTrack && Sector >= Format->FirstDirSector && Sector <= Format->LastDirSector; + return Disk->Image.fmt == D64_FMT_D81 ? 40 : 18; } -// Where a track's free count and free-sector bitmap live -FLASHMEM static void GeosBamPlaces(const struct GeosFormat *Format, uint8_t Track, struct GeosPlace *Count, struct GeosPlace *Bitmap) +// Everything that reads the directory checks the image first, so nothing follows a chain off the directory track +FLASHMEM static bool GeosCheckDisk(struct GeosDisk *Disk) { - if (Format->FixedSectors) - { - uint8_t Sector = 1 + (Track - 1) / 40, Entry = 0x10 + (Track - 1) % 40 * 6; - *Count = (struct GeosPlace){ 40, Sector, Entry }; - *Bitmap = (struct GeosPlace){ 40, Sector, (uint8_t)(Entry + 1) }; - } - else if (Track <= 35) - { - *Count = (struct GeosPlace){ 18, 0, (uint8_t)(4 + (Track - 1) * 4) }; - *Bitmap = (struct GeosPlace){ 18, 0, (uint8_t)(5 + (Track - 1) * 4) }; - } - else - { - *Count = (struct GeosPlace){ 18, 0, (uint8_t)(0xdd + Track - 36) }; - *Bitmap = (struct GeosPlace){ 53, 0, (uint8_t)((Track - 36) * 3) }; - } -} - -FLASHMEM static bool GeosSectorIsFree(const uint8_t *Bitmap, uint8_t Sector) -{ - return Bitmap[Sector / 8] & (1 << (Sector % 8)); -} - -FLASHMEM static uint8_t GeosCountFree(const struct GeosFormat *Format, uint8_t Track, const uint8_t *Bitmap, int *FirstFree) -{ - uint8_t Free = 0; - - *FirstFree = -1; - for (uint8_t s = 0; s < GeosSectorsOnTrack(Format, Track); s++) - { - if (!GeosSectorIsFree(Bitmap, s)) continue; - if (*FirstFree < 0) *FirstFree = s; - Free++; - } - return Free; -} - -// A track's BAM blocks as read; Count and Bits point into them. -struct GeosTrackBam -{ - struct GeosPlace CountAt, BitmapAt; - bool Shared; - uint8_t CountBlock[GEOS_SECTOR_SIZE], BitmapBlock[GEOS_SECTOR_SIZE]; - uint8_t *Count, *Bits, Free; - int FirstFree; -}; - -FLASHMEM static bool GeosReadTrackBam(struct GeosDisk *Disk, uint8_t Track, struct GeosTrackBam *Bam) -{ - GeosBamPlaces(Disk->Format, Track, &Bam->CountAt, &Bam->BitmapAt); - Bam->Shared = Bam->CountAt.Track == Bam->BitmapAt.Track && Bam->CountAt.Sector == Bam->BitmapAt.Sector; - if (!GeosReadTS(Disk, Bam->CountAt.Track, Bam->CountAt.Sector, Bam->CountBlock)) return false; - if (!Bam->Shared && !GeosReadTS(Disk, Bam->BitmapAt.Track, Bam->BitmapAt.Sector, Bam->BitmapBlock)) return false; - Bam->Count = Bam->CountBlock + Bam->CountAt.Byte; - Bam->Bits = (Bam->Shared ? Bam->CountBlock : Bam->BitmapBlock) + Bam->BitmapAt.Byte; - - Bam->Free = GeosCountFree(Disk->Format, Track, Bam->Bits, &Bam->FirstFree); - return true; -} - -FLASHMEM static bool GeosCheckCount(struct GeosDisk *Disk, uint8_t Track, const struct GeosTrackBam *Bam) -{ - if (*Bam->Count >= Bam->Free) return true; - snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM count for track %u disagrees with its bitmap"), Track); - return false; -} - -// Marks the first free sector of a track in use; *Sector is -1 when the track has none. -FLASHMEM static bool GeosTake(struct GeosDisk *Disk, uint8_t Track, int *Sector) -{ - struct GeosTrackBam Bam; - - if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; - *Sector = Bam.FirstFree; - if (*Sector < 0) return true; - - Bam.Bits[*Sector / 8] &= ~(1 << (*Sector % 8)); - (*Bam.Count)--; - if (!Bam.Shared && !GeosWriteTS(Disk, Bam.BitmapAt.Track, Bam.BitmapAt.Sector, Bam.BitmapBlock)) return false; - return GeosWriteTS(Disk, Bam.CountAt.Track, Bam.CountAt.Sector, Bam.CountBlock); -} - -// Refuses, before anything is written, a disk that GeosAllocate would run out on partway. -// Reads the tracks GeosAllocate would visit, in its order, so the refusals match trmouse.py's. -FLASHMEM static bool GeosCheckRoom(struct GeosDisk *Disk, unsigned Needed) -{ - struct GeosTrackBam Bam; - unsigned Found = 0; - - for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) - { - if (GeosIsReserved(Disk->Format, Track)) continue; - if (!GeosReadTrackBam(Disk, Track, &Bam) || !GeosCheckCount(Disk, Track, &Bam)) return false; - Found += Bam.Free; - if (Found >= Needed) return true; - } - return GeosFail(Disk, PSTR("the disk is full")); -} + const struct GeosTarget *Target = Disk->Target; + uint8_t Header[GEOS_SECTOR_SIZE], Track, Sector; + int Result; -FLASHMEM static bool GeosAllocate(struct GeosDisk *Disk, struct GeosTS *Where) -{ - for (uint8_t Track = 1; Track <= Disk->Format->Tracks; Track++) + if (strlen(Target->DeskTopName) > GEOS_NAME_SIZE || strlen(Target->DriverName) > GEOS_NAME_SIZE || !Target->DriverSize) { - int Sector; - if (GeosIsReserved(Disk->Format, Track)) continue; - if (!GeosTake(Disk, Track, &Sector)) return false; - if (Sector < 0) continue; - *Where = (struct GeosTS){ Track, (uint8_t)Sector }; - return true; + return GeosFail(Disk, PSTR("the GEOS target's names or driver do not fit a directory entry")); } - return GeosFail(Disk, PSTR("the disk is full")); -} - -FLASHMEM static bool GeosChainHas(const struct GeosTS *Chain, int Length, uint8_t Track, uint8_t Sector) -{ - for (int i = 0; i < Length; i++) + if (!Disk->Open) return GeosFail(Disk, PSTR("not a D64, D71 or D81 image")); + if (d64_block_read(&Disk->Image, GeosDirTrack(Disk), 0, Header)) return GeosFailed(Disk, PSTR("the image could not be read")); + if (Disk->Image.fmt == D64_FMT_D71 && !(Header[3] & D71_DOUBLE_SIDED)) { - if (Chain[i].Track == Track && Chain[i].Sector == Sector) return true; + return GeosFail(Disk, PSTR("this D71 is formatted single-sided")); } - return false; -} - -FLASHMEM static bool GeosDirChain(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) -{ - uint8_t Block[GEOS_SECTOR_SIZE]; - uint8_t Track, Sector; - - *Length = 0; - if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Block)) return false; - Track = Block[0]; - Sector = Block[1]; - if (!Track) return GeosFail(Disk, PSTR("the disk header links to no directory")); - - while (Track) + Result = d64_dir_check(&Disk->Image, &Track, &Sector); + if (Result == D64_E_NOTFOUND) return GeosFail(Disk, PSTR("the disk header links to no directory")); + if (Result == D64_E_LOOP) { - if (GeosChainHas(Chain, *Length, Track, Sector) || !GeosIsDirSector(Disk->Format, Track, Sector) || - *Length == GEOS_MAX_DIR_SECTORS) - { - snprintf(Disk->Error, sizeof Disk->Error, PSTR("the directory chain is broken at %u/%u"), Track, Sector); - return false; - } - Chain[(*Length)++] = (struct GeosTS){ Track, Sector }; - if (!GeosReadTS(Disk, Track, Sector, Block)) return false; - Track = Block[0]; - Sector = Block[1]; + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the directory chain is broken at %u/%u"), Track, Sector); + return false; } + if (Result) return GeosFailed(Disk, PSTR("the image could not be read")); return true; } -// Refuses, before anything is written, a directory that GeosGrowDirectory could not grow. -FLASHMEM static bool GeosCheckDirectoryRoom(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length) +// Whether a file is on the disk, as DOS finds it by name +FLASHMEM static bool GeosHasFile(struct GeosDisk *Disk, const char *Name, bool *Found) { - const struct GeosFormat *Format = Disk->Format; - struct GeosTrackBam Bam; + d64_entry_t Entry; + int Result = d64_find(&Disk->Image, Name, &Entry); - if (!GeosReadTrackBam(Disk, Format->DirTrack, &Bam)) return false; - for (int i = 0; i < Length; i++) - { - if (!GeosSectorIsFree(Bam.Bits, Chain[i].Sector)) continue; - snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM shows directory sector %u/%u free"), Chain[i].Track, Chain[i].Sector); - return false; - } - if (!GeosCheckCount(Disk, Format->DirTrack, &Bam)) return false; - if (Bam.FirstFree >= 0 && GeosIsDirSector(Format, Format->DirTrack, Bam.FirstFree)) return true; - return GeosFail(Disk, PSTR("the directory is full")); + *Found = Result == 1; + return Result >= 0 || GeosFailed(Disk, PSTR("the image could not be read")); } -// Chains one more directory sector, as DOS does when a directory fills. -FLASHMEM static bool GeosGrowDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length) +// Checks the image and says whether it is the target's boot disk and already holds the TR driver. +FLASHMEM static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) { - const struct GeosFormat *Format = Disk->Format; - uint8_t Block[GEOS_SECTOR_SIZE]; - int New; - - if (*Length == GEOS_MAX_DIR_SECTORS) return GeosFail(Disk, PSTR("the directory is full")); - if (!GeosCheckDirectoryRoom(Disk, Chain, *Length) || !GeosTake(Disk, Format->DirTrack, &New)) return false; - - struct GeosTS Last = Chain[*Length - 1]; - if (!GeosReadTS(Disk, Last.Track, Last.Sector, Block)) return false; - Block[0] = Format->DirTrack; - Block[1] = New; - if (!GeosWriteTS(Disk, Last.Track, Last.Sector, Block)) return false; - - memset(Block, 0, sizeof Block); - Block[1] = 0xff; - Chain[(*Length)++] = (struct GeosTS){ Format->DirTrack, (uint8_t)New }; - return GeosWriteTS(Disk, Format->DirTrack, New, Block); + return GeosCheckDisk(Disk) && GeosHasFile(Disk, Disk->Target->DeskTopName, &Found->HasDeskTop) && + GeosHasFile(Disk, Disk->Target->DriverName, &Found->HasDriver); } FLASHMEM static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) @@ -332,216 +167,148 @@ FLASHMEM static uint16_t GeosDriverDataBlocks(const struct GeosTarget *Target) return (Target->DriverSize + GEOS_DATA_PER_BLOCK - 1) / GEOS_DATA_PER_BLOCK; } +// The data chain and the info block FLASHMEM static uint16_t GeosDriverBlocks(const struct GeosTarget *Target) { return GeosDriverDataBlocks(Target) + 1; } -// Block i of the driver's data chain, linked to Next unless it is the last; returns how many of its bytes are used -FLASHMEM static uint16_t GeosDriverBlock(const struct GeosTarget *Target, uint16_t i, struct GeosTS Next, uint8_t *Block) +// Block i of the driver's data chain, linked to the next unless it is the last; returns how many of its bytes are used +FLASHMEM static uint16_t GeosDriverBlock(const struct GeosTarget *Target, uint16_t i, uint8_t NextTrack, uint8_t NextSector, + uint8_t *Block) { uint16_t Start = i * GEOS_DATA_PER_BLOCK, Length = Target->DriverSize - Start; bool Last = i + 1 == GeosDriverDataBlocks(Target); if (Length > GEOS_DATA_PER_BLOCK) Length = GEOS_DATA_PER_BLOCK; memset(Block, 0, GEOS_SECTOR_SIZE); - Block[0] = Last ? 0 : Next.Track; - Block[1] = Last ? Length + 1 : Next.Sector; + Block[0] = Last ? 0 : NextTrack; + Block[1] = Last ? Length + 1 : NextSector; memcpy(Block + 2, Target->DriverCode + Start, Length); return Length + 2; } -// Writes the driver's data chain and info block; returns the first data sector, the info sector and the block count. -FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, struct GeosTS *First, struct GeosTS *Info, uint16_t *Blocks) +// Refuses, before anything is written, a disk whose directory or free sectors would run out partway +FLASHMEM static bool GeosCheckRoom(struct GeosDisk *Disk) { - const struct GeosTarget *Target = Disk->Target; - const uint16_t DataBlocks = GeosDriverDataBlocks(Target); - struct GeosTS Here, Next = { 0, 0 }; - uint8_t Block[GEOS_SECTOR_SIZE]; + uint8_t Track, Sector; + int Result = d64_dir_room(&Disk->Image, &Track, &Sector); - if (!GeosAllocate(Disk, &Here)) return false; - *First = Here; - for (uint16_t i = 0; i < DataBlocks; i++) + if (Result == D64_E_FULL) return GeosFail(Disk, PSTR("the directory is full")); + if (Result == D64_E_BAM) { - if (i + 1 < DataBlocks && !GeosAllocate(Disk, &Next)) return false; - GeosDriverBlock(Target, i, Next, Block); - if (!GeosWriteTS(Disk, Here.Track, Here.Sector, Block)) return false; - Here = Next; + snprintf(Disk->Error, sizeof Disk->Error, PSTR("the BAM shows directory sector %u/%u free"), Track, Sector); + return false; } + if (Result < 0) return GeosFailed(Disk, PSTR("the image could not be read")); - if (!GeosAllocate(Disk, Info)) return false; - *Blocks = GeosDriverBlocks(Target); - return GeosWriteTS(Disk, Info->Track, Info->Sector, Target->DriverInfo); -} - -FLASHMEM static void GeosBuildEntry(const struct GeosTarget *Target, uint8_t *Slot, struct GeosTS First, struct GeosTS Info, uint16_t Blocks, const struct GeosDate *Date) -{ - memset(Slot + SlotType, 0, GEOS_DIR_ENTRY_SIZE - SlotType); - Slot[SlotType] = CBM_USR_CLOSED; - Slot[SlotFirst] = First.Track; - Slot[SlotFirst + 1] = First.Sector; - memset(Slot + SlotName, CBM_NAME_PAD, GEOS_NAME_SIZE); - memcpy(Slot + SlotName, Target->DriverName, strlen(Target->DriverName)); - Slot[SlotInfo] = Info.Track; - Slot[SlotInfo + 1] = Info.Sector; - Slot[SlotStructure] = GEOS_SEQUENTIAL; - Slot[SlotGeosType] = Target->InputType; - Slot[SlotDate] = Date->Year % 100; - Slot[SlotDate + 1] = Date->Month; - Slot[SlotDate + 2] = Date->Day; - Slot[SlotDate + 3] = Date->Hour; - Slot[SlotDate + 4] = Date->Minute; - Slot[SlotBlocks] = Blocks & 0xff; - Slot[SlotBlocks + 1] = Blocks >> 8; -} - -FLASHMEM static bool GeosSlotNamed(const uint8_t *Slot, const char *Name) -{ - size_t Length = strlen(Name); - - if (memcmp(Slot + SlotName, Name, Length)) return false; - for (size_t i = Length; i < GEOS_NAME_SIZE; i++) - { - if (Slot[SlotName + i] != CBM_NAME_PAD) return false; - } + Result = d64_blocks_allocatable(&Disk->Image); + if (Result < 0) return GeosFailed(Disk, PSTR("the image could not be read")); + if (Result < GeosDriverBlocks(Disk->Target)) return GeosFail(Disk, PSTR("the disk is full")); return true; } -// A slot's place in directory order, as an index over the chain's sectors and their slots -struct GeosSlotRef { int Sector, Slot; }; - -struct GeosDirScan -{ - struct GeosSlotRef Free, FirstInputDriver, Ours; - bool HasFree, HasInputDriver, HasDeskTop, HasOurs; -}; - -FLASHMEM static bool GeosScanDirectory(struct GeosDisk *Disk, const struct GeosTS *Chain, int Length, struct GeosDirScan *Scan) +// The driver as DOS saves a file, then its info block in the sector a longer file would have taken next +FLASHMEM static bool GeosWriteDriver(struct GeosDisk *Disk, d64_wfile_t *File, uint8_t *InfoTrack, uint8_t *InfoSector) { const struct GeosTarget *Target = Disk->Target; - uint8_t Block[GEOS_SECTOR_SIZE]; - memset(Scan, 0, sizeof *Scan); - for (int i = 0; i < Length; i++) + if (d64_create(&Disk->Image, Target->DriverName, CBM_USR, 0, File) || + d64_write(&Disk->Image, File, Target->DriverCode, Target->DriverSize) != Target->DriverSize || + d64_close_write(&Disk->Image, File)) { - if (!GeosReadTS(Disk, Chain[i].Track, Chain[i].Sector, Block)) return false; - for (int e = 0; e < GEOS_ENTRIES_PER_DIR; e++) - { - const uint8_t *Slot = Block + e * GEOS_DIR_ENTRY_SIZE; - struct GeosSlotRef Here = { i, e }; - bool InUse = Slot[SlotType] & CBM_TYPE_MASK; - - if (!Slot[SlotType] && !Scan->HasFree) { Scan->Free = Here; Scan->HasFree = true; } - if (!InUse) continue; - if (GeosSlotNamed(Slot, Target->DriverName)) { Scan->Ours = Here; Scan->HasOurs = true; } - if (GeosSlotNamed(Slot, Target->DeskTopName)) Scan->HasDeskTop = true; - if (Slot[SlotGeosType] == Target->InputType && !Scan->HasInputDriver) { Scan->FirstInputDriver = Here; Scan->HasInputDriver = true; } - } + return GeosFailed(Disk, PSTR("the image could not be written")); + } + *InfoTrack = File->cur_t; + *InfoSector = File->cur_s; + if (d64_alloc_next(&Disk->Image, InfoTrack, InfoSector) || + d64_block_write(&Disk->Image, *InfoTrack, *InfoSector, Target->DriverInfo)) + { + return GeosFailed(Disk, PSTR("the image could not be written")); } return true; } -FLASHMEM static bool GeosSlotBefore(struct GeosSlotRef a, struct GeosSlotRef b) +// What makes the driver's entry a GEOS file: its info block, structure, type and date, and the info block in the count +FLASHMEM static bool GeosMakeGeosEntry(struct GeosDisk *Disk, const d64_wfile_t *File, uint8_t InfoTrack, uint8_t InfoSector, + const struct GeosDate *Date) { - return a.Sector < b.Sector || (a.Sector == b.Sector && a.Slot < b.Slot); + uint8_t Block[GEOS_SECTOR_SIZE], *Slot = Block + File->dir_slot * GEOS_DIR_ENTRY_SIZE; + uint16_t Blocks = File->blocks + 1; + + if (d64_block_read(&Disk->Image, File->dir_t, File->dir_s, Block)) return GeosFailed(Disk, PSTR("the image could not be read")); + Slot[SlotInfo] = InfoTrack; + Slot[SlotInfo + 1] = InfoSector; + Slot[SlotStructure] = GEOS_SEQUENTIAL; + Slot[SlotGeosType] = Disk->Target->InputType; + Slot[SlotDate] = Date->Year % 100; + Slot[SlotDate + 1] = Date->Month; + Slot[SlotDate + 2] = Date->Day; + Slot[SlotDate + 3] = Date->Hour; + Slot[SlotDate + 4] = Date->Minute; + Slot[SlotBlocks] = Blocks & 0xff; + Slot[SlotBlocks + 1] = Blocks >> 8; + if (d64_block_write(&Disk->Image, File->dir_t, File->dir_s, Block)) return GeosFailed(Disk, PSTR("the image could not be written")); + return true; } -FLASHMEM static bool GeosSwapEntries(struct GeosDisk *Disk, const struct GeosTS *Chain, struct GeosSlotRef a, struct GeosSlotRef b) +FLASHMEM static bool GeosSwapEntries(struct GeosDisk *Disk, const d64_entry_t *A, const d64_wfile_t *B) { uint8_t BlockA[GEOS_SECTOR_SIZE], BlockB[GEOS_SECTOR_SIZE], Saved[GEOS_DIR_ENTRY_SIZE]; - bool Shared = a.Sector == b.Sector; - uint8_t *SlotA = BlockA + a.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; - uint8_t *SlotB = (Shared ? BlockA : BlockB) + b.Slot * GEOS_DIR_ENTRY_SIZE + SlotType; + bool Shared = A->dir_t == B->dir_t && A->dir_s == B->dir_s; + uint8_t *SlotA = BlockA + A->dir_slot * GEOS_DIR_ENTRY_SIZE + SlotType; + uint8_t *SlotB = (Shared ? BlockA : BlockB) + B->dir_slot * GEOS_DIR_ENTRY_SIZE + SlotType; const size_t EntryBytes = GEOS_DIR_ENTRY_SIZE - SlotType; - if (!GeosReadTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA)) return false; - if (!Shared && !GeosReadTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; + if (d64_block_read(&Disk->Image, A->dir_t, A->dir_s, BlockA)) return GeosFailed(Disk, PSTR("the image could not be read")); + if (!Shared && d64_block_read(&Disk->Image, B->dir_t, B->dir_s, BlockB)) return GeosFailed(Disk, PSTR("the image could not be read")); memcpy(Saved, SlotA, EntryBytes); memcpy(SlotA, SlotB, EntryBytes); memcpy(SlotB, Saved, EntryBytes); - if (!Shared && !GeosWriteTS(Disk, Chain[b.Sector].Track, Chain[b.Sector].Sector, BlockB)) return false; - return GeosWriteTS(Disk, Chain[a.Sector].Track, Chain[a.Sector].Sector, BlockA); -} - -FLASHMEM static bool GeosWriteEntry(struct GeosDisk *Disk, struct GeosTS Where, int Slot, const struct GeosDate *Date) -{ - struct GeosTS First, Info; - uint16_t Blocks; - uint8_t Block[GEOS_SECTOR_SIZE]; - - if (!GeosWriteDriver(Disk, &First, &Info, &Blocks)) return false; - if (!GeosReadTS(Disk, Where.Track, Where.Sector, Block)) return false; - GeosBuildEntry(Disk->Target, Block + Slot * GEOS_DIR_ENTRY_SIZE, First, Info, Blocks, Date); - return GeosWriteTS(Disk, Where.Track, Where.Sector, Block); -} - -FLASHMEM static inline const struct GeosFormat *GeosFormatForSize(uint32_t Size) -{ - for (size_t i = 0; i < sizeof GeosFormats / sizeof GeosFormats[0]; i++) - { - if (GeosFormats[i].Size == Size) return &GeosFormats[i]; - } - return NULL; + if (!Shared && d64_block_write(&Disk->Image, B->dir_t, B->dir_s, BlockB)) return GeosFailed(Disk, PSTR("the image could not be written")); + if (d64_block_write(&Disk->Image, A->dir_t, A->dir_s, BlockA)) return GeosFailed(Disk, PSTR("the image could not be written")); + return true; } -FLASHMEM static bool GeosReadDirectory(struct GeosDisk *Disk, struct GeosTS *Chain, int *Length, struct GeosDirScan *Scan) +// GEOS 128 boots with the first input driver in the directory, so the new one trades places with an earlier one +FLASHMEM static bool GeosMoveFirst(struct GeosDisk *Disk, const d64_wfile_t *File) { - uint8_t Header[GEOS_SECTOR_SIZE]; + d64_dir_t Dir; + d64_entry_t Entry; + int Result; - const struct GeosTarget *Target = Disk->Target; - - if (strlen(Target->DeskTopName) > GEOS_NAME_SIZE || strlen(Target->DriverName) > GEOS_NAME_SIZE || !Target->DriverSize) - { - return GeosFail(Disk, PSTR("the GEOS target's names or driver do not fit a directory entry")); - } - if (!Disk->Format) return GeosFail(Disk, PSTR("not a D64, D71 or D81 without error bytes")); - if (!GeosReadTS(Disk, Disk->Format->DirTrack, 0, Header)) return false; - if (Disk->Format->TwoSided && !(Header[3] & D71_DOUBLE_SIDED)) + if (d64_dir_first(&Disk->Image, &Dir)) return GeosFailed(Disk, PSTR("the image could not be read")); + while ((Result = d64_dir_next(&Disk->Image, &Dir, &Entry)) > 0) { - return GeosFail(Disk, PSTR("this D71 is formatted single-sided")); + const uint8_t *Slot = Dir.buf + Entry.dir_slot * GEOS_DIR_ENTRY_SIZE; + if (!(Slot[SlotType] & CBM_TYPE_MASK) || Slot[SlotGeosType] != Disk->Target->InputType) continue; + if (Entry.dir_t == File->dir_t && Entry.dir_s == File->dir_s && Entry.dir_slot == File->dir_slot) return true; + return GeosSwapEntries(Disk, &Entry, File); } - if (!GeosDirChain(Disk, Chain, Length)) return false; - return GeosScanDirectory(Disk, Chain, *Length, Scan); -} - -// Checks the image and says whether it is the target's boot disk and already holds the TR driver. -FLASHMEM static inline bool GeosInspect(struct GeosDisk *Disk, struct GeosBootDisk *Found) -{ - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; - struct GeosDirScan Scan; - int Length; - - if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; - Found->HasDeskTop = Scan.HasDeskTop; - Found->HasDriver = Scan.HasOurs; - return true; + return Result == 0 || GeosFailed(Disk, PSTR("the image could not be read")); } -// Writes the driver ahead of every other input driver of its type: GEOS 128 boots with the first one in the directory. // Every refusal but a failed block read or write comes before the first write, leaving the image as it was. FLASHMEM static inline bool GeosInstallDriver(struct GeosDisk *Disk, const struct GeosDate *Date) { - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS]; - struct GeosDirScan Scan; - int Length; + d64_wfile_t File; + uint8_t InfoTrack, InfoSector; + bool Present; - if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; - if (Scan.HasOurs) + if (!GeosCheckDisk(Disk) || !GeosHasFile(Disk, Disk->Target->DriverName, &Present)) return false; + if (Present) { snprintf(Disk->Error, sizeof Disk->Error, PSTR("%s is already on this disk"), Disk->Target->DriverName); return false; } - if (!Scan.HasFree && !GeosCheckDirectoryRoom(Disk, Chain, Length)) return false; - if (!GeosCheckRoom(Disk, GeosDriverBlocks(Disk->Target))) return false; - if (!Scan.HasFree) - { - if (!GeosGrowDirectory(Disk, Chain, &Length)) return false; - Scan.Free = (struct GeosSlotRef){ Length - 1, 0 }; - } + if (!GeosCheckRoom(Disk)) return false; + return GeosWriteDriver(Disk, &File, &InfoTrack, &InfoSector) && GeosMakeGeosEntry(Disk, &File, InfoTrack, InfoSector, Date) && + GeosMoveFirst(Disk, &File); +} - if (!GeosWriteEntry(Disk, Chain[Scan.Free.Sector], Scan.Free.Slot, Date)) return false; - if (!Scan.HasInputDriver || !GeosSlotBefore(Scan.FirstInputDriver, Scan.Free)) return true; - return GeosSwapEntries(Disk, Chain, Scan.FirstInputDriver, Scan.Free); +FLASHMEM static bool GeosOnDisk(const struct GeosDisk *Disk, uint8_t Track, uint8_t Sector) +{ + return d64_sector_offset(&Disk->Image, Track, Sector) >= 0; } // Makes the copy's TR mouse driver the firmware's, in place: a copy outlives the firmware that set it up, and an older @@ -550,47 +317,46 @@ FLASHMEM static inline bool GeosRefreshDriver(struct GeosDisk *Disk, bool *Chang { const struct GeosTarget *Target = Disk->Target; const uint16_t DataBlocks = GeosDriverDataBlocks(Target); - struct GeosTS Chain[GEOS_MAX_DIR_SECTORS], First, Info, At; - uint8_t Block[GEOS_SECTOR_SIZE], Wanted[GEOS_SECTOR_SIZE]; - const uint8_t *Slot; - struct GeosDirScan Scan; + uint8_t Block[GEOS_SECTOR_SIZE], Wanted[GEOS_SECTOR_SIZE], Track, Sector; + d64_entry_t Entry; uint16_t Blocks = 0; - int Length; + int Found; *Changed = false; - if (!GeosReadDirectory(Disk, Chain, &Length, &Scan)) return false; - if (!Scan.HasOurs) return true; - if (!GeosReadTS(Disk, Chain[Scan.Ours.Sector].Track, Chain[Scan.Ours.Sector].Sector, Block)) return false; - Slot = Block + Scan.Ours.Slot * GEOS_DIR_ENTRY_SIZE; - First = (struct GeosTS){ Slot[SlotFirst], Slot[SlotFirst + 1] }; - Info = (struct GeosTS){ Slot[SlotInfo], Slot[SlotInfo + 1] }; - for (At = First; At.Track && At.Track <= Disk->Format->Tracks && Blocks <= DataBlocks; Blocks++) + if (!GeosCheckDisk(Disk)) return false; + Found = d64_find(&Disk->Image, Target->DriverName, &Entry); + if (Found < 0) return GeosFailed(Disk, PSTR("the image could not be read")); + if (!Found) return true; + for (Track = Entry.track, Sector = Entry.sector; Track && GeosOnDisk(Disk, Track, Sector) && Blocks <= DataBlocks; Blocks++) { - if (!GeosReadTS(Disk, At.Track, At.Sector, Block)) return false; - At = (struct GeosTS){ Block[0], Block[1] }; + if (d64_block_read(&Disk->Image, Track, Sector, Block)) return GeosFailed(Disk, PSTR("the image could not be read")); + Track = Block[0]; + Sector = Block[1]; } - if (At.Track || Blocks != DataBlocks || !Info.Track || Info.Track > Disk->Format->Tracks) + if (Track || Blocks != DataBlocks || !GeosOnDisk(Disk, Entry.ss_t, Entry.ss_s)) { snprintf(Disk->Error, sizeof Disk->Error, PSTR("its %s is an old one; delete this copy"), Target->DriverName); return false; } - for (uint16_t i = 0; First.Track; i++) + Track = Entry.track; + Sector = Entry.sector; + for (uint16_t i = 0; Track; i++) { - struct GeosTS Next; uint16_t Used; - if (!GeosReadTS(Disk, First.Track, First.Sector, Block)) return false; - Next = (struct GeosTS){ Block[0], Block[1] }; - Used = GeosDriverBlock(Target, i, Next, Wanted); + if (d64_block_read(&Disk->Image, Track, Sector, Block)) return GeosFailed(Disk, PSTR("the image could not be read")); + Used = GeosDriverBlock(Target, i, Block[0], Block[1], Wanted); if (memcmp(Block, Wanted, Used)) { - if (!GeosWriteTS(Disk, First.Track, First.Sector, Wanted)) return false; + if (d64_block_write(&Disk->Image, Track, Sector, Wanted)) return GeosFailed(Disk, PSTR("the image could not be written")); *Changed = true; } - First = Block[0] ? Next : (struct GeosTS){ 0, 0 }; + Track = Block[0]; + Sector = Block[1]; } - if (!GeosReadTS(Disk, Info.Track, Info.Sector, Block)) return false; + if (d64_block_read(&Disk->Image, Entry.ss_t, Entry.ss_s, Block)) return GeosFailed(Disk, PSTR("the image could not be read")); if (!memcmp(Block, Target->DriverInfo, GEOS_SECTOR_SIZE)) return true; *Changed = true; - return GeosWriteTS(Disk, Info.Track, Info.Sector, Target->DriverInfo); + if (d64_block_write(&Disk->Image, Entry.ss_t, Entry.ss_s, Target->DriverInfo)) return GeosFailed(Disk, PSTR("the image could not be written")); + return true; } diff --git a/Source/Teensy/GeosLauncher.ino b/Source/Teensy/GeosLauncher.ino index 4bba1049..2f346746 100644 --- a/Source/Teensy/GeosLauncher.ino +++ b/Source/Teensy/GeosLauncher.ino @@ -25,17 +25,14 @@ FLASHMEM static bool GeosFileWrite(void *Context, uint32_t Offset, const uint8_t return Image->seek(Offset) && Image->write(Block, GEOS_SECTOR_SIZE) == GEOS_SECTOR_SIZE; } -FLASHMEM static struct GeosDisk GeosDiskOn(const struct GeosLauncher *Launcher, File *Image) +FLASHMEM static void GeosDiskOn(const struct GeosLauncher *Launcher, File *Image, struct GeosDisk *Disk) { - struct GeosDisk Disk = {}; - uint64_t Size = Image->size(); - - Disk.Format = Size <= UINT32_MAX ? GeosFormatForSize(Size) : NULL; - Disk.Target = &Launcher->Target; - Disk.Context = Image; - Disk.Read = GeosFileRead; - Disk.Write = GeosFileWrite; - return Disk; + memset(Disk, 0, sizeof *Disk); + Disk->Target = &Launcher->Target; + Disk->Context = Image; + Disk->Read = GeosFileRead; + Disk->Write = GeosFileWrite; + GeosDiskOpen(Disk, Image->size()); } FLASHMEM static bool GeosHasImageExtension(const char *Name) @@ -92,7 +89,7 @@ FLASHMEM static bool GeosInspectFile(const struct GeosLauncher *Launcher, const if (Error) snprintf(Error, ErrorSize, PSTR("the image could not be opened")); return false; } - Disk = GeosDiskOn(Launcher, &Image); + GeosDiskOn(Launcher, &Image, &Disk); Inspected = GeosInspect(&Disk, Found); if (!Inspected && Error) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); Image.close(); @@ -143,7 +140,7 @@ FLASHMEM static bool GeosPatchFile(const struct GeosLauncher *Launcher, const ch snprintf(Error, ErrorSize, PSTR("the image could not be opened")); return false; } - Disk = GeosDiskOn(Launcher, &Image); + GeosDiskOn(Launcher, &Image, &Disk); Patched = GeosInstallDriver(&Disk, &Today); if (!Patched) snprintf(Error, ErrorSize, PSTR("%s"), Disk.Error); Image.close(); @@ -296,7 +293,7 @@ FLASHMEM static bool GeosStart(const struct GeosLauncher *Launcher, const char * SendMsgPrintfln(PSTR("%s\r\ncould not be opened"), Name); return false; } - Disk = GeosDiskOn(Launcher, &Image); + GeosDiskOn(Launcher, &Image, &Disk); Built = GeosRefreshDriver(&Disk, &Changed) && GeosPrepareConfigure(&Disk, RAM_Image, RAM_ImageSize, &Changed) && (Launcher->Machine == GeosC128 ? GeosBuildC128Boot(&Disk, &Now, GeosIOHandler() == IOH_GeoRAM ? GEOS_REGS_BESIDE_GEORAM : GEOS_REGS_BESIDE_REU, RAM_Image, RAM_ImageSize, &Boot, &Length, &ClockSet) 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 new file mode 100644 index 00000000..78f78a51 --- /dev/null +++ b/Source/Teensy/tests/disk-image.test.js @@ -0,0 +1,83 @@ +// 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`); +}); + +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 new file mode 100644 index 00000000..b05dfcd2 --- /dev/null +++ b/Source/Teensy/tests/disk_image_harness.c @@ -0,0 +1,67 @@ +// 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|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 + +#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, bool Exact) +{ + static const char *const Names[] = { "d64", "d71", "d81" }; + size_t Size = (size_t)strtoul(Bytes, NULL, 10); + d64_t Disk; + + if (Exact ? d64_open_exact(&Disk, NULL, NULL, Size) : d64_open_size(&Disk, NULL, NULL, Size)) + { + 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], false); + if (argc == 3 && !strcmp(argv[1], "exact")) return SizeOf(argv[2], true); + return 2; +} diff --git a/Source/Teensy/tests/geos-disk-patch.test.js b/Source/Teensy/tests/geos-disk-patch.test.js index e2a90ab5..25ffce10 100644 --- a/Source/Teensy/tests/geos-disk-patch.test.js +++ b/Source/Teensy/tests/geos-disk-patch.test.js @@ -1,5 +1,6 @@ // SPDX-License-Identifier: MIT -// The firmware's GEOS boot-disk patch, held byte for byte to tools/geos/trmouse.py on synthetic boot disks. +// The firmware's GEOS boot-disk patch, held byte for byte to tools/geos/trmouse.py on synthetic boot disks. Both +// allocate as 1541 DOS does (docs/Architecture/Disk-Images.md), so the expected bytes come from the Python model. import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -131,10 +132,34 @@ function withOneDataSector(disk) { return disk; } +const withErrorBytes = (disk) => { + const sectors = disk.image.length / SECTOR; + return { ...disk, image: Buffer.concat([disk.image, Buffer.alloc(sectors, 1)]) }; +}; + +const firstDataTrack = (kind) => (kind === 'd81' ? 39 : 17); + const PATCHED_CASES = { 'a blank boot disk': blankDisk, 'a disk whose first input driver comes earlier': withDriver, 'a full first directory sector': fullDirectory, + 'a disk with error bytes': (kind) => withErrorBytes(withDriver(kind)), + 'a BAM count below its bitmap': (kind) => { + const disk = blankDisk(kind); + disk.image[at(disk, bamPlaces(kind, 1).count)] -= 1; + return disk; + }, + 'a first data track with one sector free': (kind) => { + const disk = withDriver(kind); + const first = firstDataTrack(kind); + setTrackFree(disk, first, [disk.format.sectors(first) - 1]); + return disk; + }, + 'a first data track whose count says full': (kind) => { + const disk = blankDisk(kind); + disk.image[at(disk, bamPlaces(kind, firstDataTrack(kind)).count)] = 0; + return disk; + }, }; const REFUSED_CASES = { @@ -143,11 +168,6 @@ const REFUSED_CASES = { putEntry(disk, 2, { name: MOUSE_NAME, geosType: INPUT_128 }); return disk; }, - 'a BAM count below its bitmap': (kind) => { - const disk = blankDisk(kind); - disk.image[at(disk, bamPlaces(kind, 1).count)] -= 1; - return disk; - }, 'a header linking outside the directory': (kind) => { const disk = blankDisk(kind); disk.image[offset(disk.format, disk.format.dirTrack, 0)] = 1; @@ -161,6 +181,11 @@ const REFUSED_CASES = { setTrackFree(disk, disk.format.dirTrack, [0]); return disk; }, + 'a full directory whose track has only sectors before the directory free': (kind) => { + const disk = fullDirectory(kind); + setTrackFree(disk, disk.format.dirTrack, allSectors(disk, disk.format.dirTrack).filter((s) => s < disk.format.firstDir)); + return disk; + }, 'a full directory whose BAM frees its sector': (kind) => { const disk = fullDirectory(kind); const free = allSectors(disk, disk.format.dirTrack).filter((s) => s >= disk.format.firstDir); @@ -243,6 +268,35 @@ test('the firmware patch matches trmouse.py', { skip }, async (t) => { }); } + await t.test('d64: a 40-track disk takes the driver on its first 35 tracks', () => { + const disk = withDriver('d64'); + const image = Buffer.concat([disk.image, Buffer.alloc(5 * 17 * SECTOR)]); + const { python, firmware } = patchBoth(dir, 'd64-40-tracks', image); + assert.equal(python.status, 0, python.stderr); + assert.equal(firmware.status, 0, firmware.stderr); + const patched = fs.readFileSync(firmware.out); + assert.deepEqual(maskMouseDate(patched), maskMouseDate(fs.readFileSync(python.out))); + assert.deepEqual(patched.subarray(disk.image.length), image.subarray(disk.image.length), 'tracks 36 to 40 are not in the BAM'); + }); + + await t.test('d81: a growing directory never takes a BAM sector the BAM shows free', () => { + const disk = fullDirectory('d81'); + setTrackFree(disk, 40, [1, 4]); + const { python, firmware } = patchBoth(dir, 'd81-bam-sector-free', disk.image); + assert.equal(python.status, 0, python.stderr); + assert.equal(firmware.status, 0, firmware.stderr); + const patched = fs.readFileSync(firmware.out); + assert.deepEqual(maskMouseDate(patched), maskMouseDate(fs.readFileSync(python.out))); + const lastDir = offset(disk.format, 40, 3); + assert.deepEqual([...patched.subarray(lastDir, lastDir + 2)], [40, 4], 'the directory grows into 40/4, not BAM sector 40/1'); + }); + + await t.test('refuses an image of no known size', () => { + const { python, firmware } = patchBoth(dir, 'odd-size', blankDisk('d64').image.subarray(0, 100 * SECTOR)); + assert.equal(firmware.status, 1); + assert.notEqual(python.status, 0); + }); + await t.test('d71: refuses a single-sided disk', () => { const disk = blankDisk('d71'); disk.image[offset(disk.format, 18, 0) + 3] = 0; diff --git a/Source/Teensy/tests/geos_boot_harness.c b/Source/Teensy/tests/geos_boot_harness.c index 0c32b997..8bc6966d 100644 --- a/Source/Teensy/tests/geos_boot_harness.c +++ b/Source/Teensy/tests/geos_boot_harness.c @@ -9,7 +9,7 @@ #include "../GEOS/GeosTargets.h" -#define MAX_IMAGE 819200 +#define MAX_IMAGE 822400 #define MAX_PRG (128 * 1024) static uint8_t Image[MAX_IMAGE + 1], Prg[MAX_PRG]; @@ -33,7 +33,7 @@ static bool WriteBlock(void *Context, uint32_t Offset, const uint8_t *Block) int main(int argc, char **argv) { struct GeosDate Now = { 26, 10, 21, 15, 7 }; - struct GeosDisk Disk = { 0 }; + static struct GeosDisk Disk; uint32_t Length, BootSize = 0; const uint8_t *Boot = NULL; const char *RegsPage = getenv("REGS_PAGE"); @@ -50,11 +50,11 @@ int main(int argc, char **argv) Size = fread(Image, 1, sizeof Image, File); fclose(File); - Disk.Format = GeosFormatForSize(Size); Disk.Target = Is128 ? &Geos128Launcher.Target : &Geos64Launcher.Target; Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; + GeosDiskOpen(&Disk, Size); Built = GeosRefreshDriver(&Disk, &Refreshed) && GeosPrepareConfigure(&Disk, Prg, sizeof Prg, &Changed) && (Is128 ? GeosBuildC128Boot(&Disk, &Now, RegsPage ? (uint8_t)strtol(RegsPage, NULL, 16) : GEOS_REGS_BESIDE_GEORAM, Prg, sizeof Prg, &Boot, &BootSize, &ClockSet) : GeosBuildBootPrg(&Disk, &Now, Prg, sizeof Prg, &Length, &ClockSet)); diff --git a/Source/Teensy/tests/geos_disk_patch_harness.c b/Source/Teensy/tests/geos_disk_patch_harness.c index 7bff4d96..e73f4490 100644 --- a/Source/Teensy/tests/geos_disk_patch_harness.c +++ b/Source/Teensy/tests/geos_disk_patch_harness.c @@ -7,7 +7,7 @@ #include "../GEOS/GeosTargets.h" -#define MAX_IMAGE 819200 +#define MAX_IMAGE 822400 // One byte past the largest image, so an oversized file reads as a size no format has static uint8_t Image[MAX_IMAGE + 1], Original[MAX_IMAGE + 1]; @@ -41,7 +41,7 @@ static int Inspect(struct GeosDisk *Disk) int main(int argc, char **argv) { const struct GeosDate Date = { 26, 10, 16, 12, 0 }; - struct GeosDisk Disk = { 0 }; + static struct GeosDisk Disk; FILE *In, *Out; size_t Size; bool Inspecting; @@ -54,11 +54,11 @@ int main(int argc, char **argv) fclose(In); memcpy(Original, Image, Size); - Disk.Format = GeosFormatForSize(Size); Disk.Target = &Geos128Launcher.Target; Disk.Context = Image; Disk.Read = ReadBlock; Disk.Write = WriteBlock; + GeosDiskOpen(&Disk, Size); if (Inspecting) return Inspect(&Disk); if (!GeosInstallDriver(&Disk, &Date)) { diff --git a/Source/Teensy/tests/menu-disk.test.js b/Source/Teensy/tests/menu-disk.test.js new file mode 100644 index 00000000..c431ee1e --- /dev/null +++ b/Source/Teensy/tests/menu-disk.test.js @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MIT +// What the TR menu reads from a D64, D71 or D81 (Source/Teensy/DiskImage/MenuDisk.h, used by D64.ino). +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/menu_disk_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`; + +const SECTOR = 256; +const KIND = { d64: 0, d71: 1, d81: 2 }; +const zoneSectors = (track) => { + const zone = (track - 1) % 35 + 1; + return zone <= 17 ? 21 : zone <= 24 ? 19 : zone <= 30 ? 18 : 17; +}; +const sectorsOn = (kind, track) => (kind === 'd81' ? 40 : kind === 'd64' && track > 35 ? 17 : zoneSectors(track)); + +function offset(kind, track, sector) { + let blocks = sector; + for (let t = 1; t < track; t++) blocks += sectorsOn(kind, t); + return blocks * SECTOR; +} + +// A file laid along the given sectors, its last one partly used; returns its bytes +function putFile(image, kind, sectors, lastLength) { + const parts = []; + sectors.forEach(([track, sector], i) => { + const last = i === sectors.length - 1; + const length = last ? lastLength : SECTOR - 2; + const data = Buffer.from(Array.from({ length }, (_, n) => (n * 13 + i * 7 + track) & 0xff)); + const at = offset(kind, track, sector); + image[at] = last ? 0 : sectors[i + 1][0]; + image[at + 1] = last ? length + 1 : sectors[i + 1][1]; + data.copy(image, at + 2); + parts.push(data); + }); + return Buffer.concat(parts); +} + +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'); + harnessPath = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'menu-disk-')), '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; +} + +function load(kind, image, [track, sector], capacity = 1 << 20) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'menu-disk-run-')); + try { + const input = path.join(dir, 'in'), out = path.join(dir, 'out'); + fs.writeFileSync(input, image); + const ran = spawnSync(harness(), ['read', KIND[kind], input, track, sector, capacity, out], { encoding: 'utf8' }); + assert.equal(ran.status, 0, ran.stderr); + const said = ran.stdout.trim(); + return { said, bytes: said.startsWith('length=') ? fs.readFileSync(out) : null }; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +const IMAGES = [ + { label: 'd64', kind: 'd64', size: 174848, chain: [[17, 0], [1, 5], [35, 16]] }, + { label: 'd64 with error bytes', kind: 'd64', size: 175531, chain: [[18, 18], [35, 16]] }, + { label: 'd64 with 40 tracks', kind: 'd64', size: 196608, chain: [[35, 16], [40, 16]] }, + { label: 'd71', kind: 'd71', size: 349696, chain: [[35, 0], [36, 20], [70, 16]] }, + { label: 'd81', kind: 'd81', size: 819200, chain: [[1, 0], [40, 39], [80, 39]] }, + { label: 'd81 with error bytes', kind: 'd81', size: 822400, chain: [[80, 0]] }, +]; + +for (const { label, kind, size, chain } of IMAGES) { + test(`${label}: a file loads whole, its last sector only as far as its link says`, { skip }, () => { + const image = Buffer.alloc(size); + const expected = putFile(image, kind, chain, 100); + const { said, bytes } = load(kind, image, chain[0]); + assert.equal(said, `length=${expected.length}`); + assert.deepEqual(bytes, expected); + }); +} + +test('a short D64 (Realm.d64 is 171520 bytes) opens, and a link past its end is a broken file', { skip }, () => { + const image = Buffer.alloc(171520); + const expected = putFile(image, 'd64', [[1, 0], [1, 10]], 3); + assert.deepEqual(load('d64', image, [1, 0]).bytes, expected); + putFile(image, 'd64', [[2, 0], [35, 16]], 3); + assert.equal(load('d64', image, [2, 0]).said, 'broken'); +}); + +test('a chain that loops, or links off the image, is a broken file', { skip }, () => { + const image = Buffer.alloc(174848); + putFile(image, 'd64', [[1, 0], [1, 1]], 10); + image[offset('d64', 1, 1)] = 1; + image[offset('d64', 1, 1) + 1] = 0; + assert.equal(load('d64', image, [1, 0]).said, 'broken'); + putFile(image, 'd64', [[2, 0], [36, 0]], 10); + assert.equal(load('d64', image, [2, 0]).said, 'broken'); +}); + +test('a file longer than the buffer is too big, and one exactly its size fits', { skip }, () => { + const image = Buffer.alloc(174848); + const expected = putFile(image, 'd64', [[1, 0], [1, 1]], 100); + assert.equal(load('d64', image, [1, 0], expected.length - 1).said, 'too big'); + assert.equal(load('d64', image, [1, 0], expected.length).said, `length=${expected.length}`); +}); + +test('a file of a size outside its kind\'s is refused', { skip }, () => { + for (const [kind, size] of [['d64', 171519], ['d64', 197377], ['d71', 349695], ['d71', 351063], ['d81', 819199], ['d81', 822401]]) { + assert.equal(load(kind, Buffer.alloc(size), [1, 0]).said, 'refused', `${kind} of ${size} bytes`); + } +}); + +const DIR = { d64: [18, 1], d71: [18, 1], d81: [40, 3] }; +const PAD = 0xa0; + +// Directory sectors along the chain given, entries placed by [sector index, slot, type, name] +function directory(kind, size, chain, entries, headerLink = chain[0]) { + const image = Buffer.alloc(size); + const header = offset(kind, DIR[kind][0], 0); + image[header] = headerLink[0]; + image[header + 1] = headerLink[1]; + chain.forEach(([track, sector], i) => { + const at = offset(kind, track, sector); + const next = chain[i + 1]; + image[at] = next ? next[0] : 0; + image[at + 1] = next ? next[1] : 0xff; + }); + for (const [index, slot, type, name] of entries) { + const at = offset(kind, ...chain[index]) + slot * 32; + image[at + 2] = type; + image[at + 3] = 1; + image[at + 4] = slot; + image.fill(PAD, at + 5, at + 21); + image.write(name, at + 5, 'latin1'); + } + return image; +} + +function list(kind, image, limit = 1000) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'menu-disk-list-')); + try { + const input = path.join(dir, 'in'); + fs.writeFileSync(input, image); + const ran = spawnSync(harness(), ['list', KIND[kind], input, limit], { encoding: 'utf8' }); + assert.equal(ran.status, 0, ran.stderr); + return ran.stdout.trim().split('\n'); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +for (const [kind, size] of [['d64', 174848], ['d71', 349696], ['d81', 819200]]) { + test(`${kind}: the listing follows the directory chain and leaves deleted entries out`, { skip }, () => { + const [track, first] = DIR[kind]; + const image = directory(kind, size, [[track, first], [track, first + 3]], [ + [0, 0, 0x82, 'GAME'], + [0, 1, 0x00, 'DELETED'], + [0, 3, 0x81, 'AFTER A GAP'], + [1, 0, 0x83, 'NEXT SECTOR'], + ]); + assert.deepEqual(list(kind, image), ['82 1/0 GAME', '81 1/3 AFTER A GAP', '83 1/0 NEXT SECTOR', 'end']); + }); +} + +test('the listing starts at the first directory sector, whatever the header links to', { skip }, () => { + const image = directory('d64', 174848, [[18, 1]], [[0, 0, 0x82, 'FIRST']], [18, 4]); + assert.deepEqual(list('d64', image), ['82 1/0 FIRST', 'end']); +}); + +test('a directory chain that loops lists what it read and reports it broken', { skip }, () => { + const image = directory('d64', 174848, [[18, 1], [18, 4]], [[0, 0, 0x82, 'ONE'], [1, 0, 0x82, 'TWO']]); + image[offset('d64', 18, 4)] = 18; + image[offset('d64', 18, 4) + 1] = 1; + const lines = list('d64', image); + assert.deepEqual(lines.slice(0, 2), ['82 1/0 ONE', '82 1/0 TWO']); + assert.equal(lines.at(-1), 'broken'); +}); + +test('the listing stops when the menu is full', { skip }, () => { + const image = directory('d64', 174848, [[18, 1]], [[0, 0, 0x82, 'ONE'], [0, 1, 0x82, 'TWO'], [0, 2, 0x82, 'THREE']]); + assert.deepEqual(list('d64', image, 2), ['82 1/0 ONE', '82 1/1 TWO', 'end']); +}); diff --git a/Source/Teensy/tests/menu_disk_harness.c b/Source/Teensy/tests/menu_disk_harness.c new file mode 100644 index 00000000..70f9349a --- /dev/null +++ b/Source/Teensy/tests/menu_disk_harness.c @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// menu_disk_harness read KIND IMAGE TRACK SECTOR CAPACITY OUT: what the TR menu loads from a D64, D71 or D81 (KIND 0, 1 +// or 2) for the file starting at TRACK/SECTOR. Writes the bytes to OUT and prints "length=", or prints "refused" +// (not an image of that kind), "broken" or "too big". +// menu_disk_harness list KIND IMAGE LIMIT: the files the menu lists, one a line as " / ", +// at most LIMIT of them, then "end" or "broken". +#include +#include + +#include "../DiskImage/MenuDisk.h" + +static uint8_t Image[822400 + 1], Out[1 << 20]; +static size_t Size; + +static int ReadSector(void *Context, uint8_t Track, uint8_t Sector, uint8_t *Block) +{ + long Offset = d64_sector_offset((d64_t *)Context, Track, Sector); + if (Offset < 0 || (size_t)Offset + D64_SECTOR > Size) return -1; + memcpy(Block, Image + Offset, D64_SECTOR); + return 0; +} + +static int Left; + +static bool Add(void *Context, const uint8_t *Name, uint8_t Type, uint8_t Track, uint8_t Sector) +{ + int Length = 16; + + (void)Context; + if (!Left) return false; + Left--; + while (Length && Name[Length - 1] == 0xa0) Length--; + printf("%02x %u/%u %.*s\n", Type, Track, Sector, Length, (const char *)Name); + return true; +} + +int main(int argc, char **argv) +{ + d64_t Disk; + FILE *File; + int32_t Length; + bool Listing = argc == 5 && !strcmp(argv[1], "list"); + + if ((!Listing && (argc != 8 || strcmp(argv[1], "read"))) || !(File = fopen(argv[3], "rb"))) return 2; + Size = fread(Image, 1, sizeof Image, File); + fclose(File); + if (!MenuDiskOpen(&Disk, (enum MenuDiskKind)atoi(argv[2]), Size, ReadSector, &Disk)) + { + printf("refused\n"); + return 0; + } + if (Listing) + { + Left = atoi(argv[4]); + printf("%s\n", MenuDiskList(&Disk, Add, NULL) ? "broken" : "end"); + return 0; + } + Length = MenuDiskReadFile(&Disk, (uint8_t)atoi(argv[4]), (uint8_t)atoi(argv[5]), Out, (uint32_t)atol(argv[6])); + if (Length == MENUDISK_BROKEN) printf("broken\n"); + else if (Length == MENUDISK_TOO_BIG) printf("too big\n"); + else + { + if (!(File = fopen(argv[7], "wb")) || fwrite(Out, 1, (size_t)Length, File) != (size_t)Length) return 2; + fclose(File); + printf("length=%d\n", (int)Length); + } + return 0; +} 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 new file mode 100644 index 00000000..a3715914 --- /dev/null +++ b/docs/Architecture/Disk-Images.md @@ -0,0 +1,75 @@ +# 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. | +| `MenuDisk.h` | The TR menu's read-only view of an image (`D64.ino`): opening by the kind the file's extension names, and loading a file. | + +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 | + +`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. | +| GEOS (`GEOS/GeosDiskPatch.h`, `GEOS/GeosBoot.h`) | `d64_open_exact`, `d64_dir_check` before anything reads the directory, `d64_find` and the file reader for the boot file, the directory iterator and the file reader for setting up CONFIGURE, `d64_block_read`/`d64_block_write` for bringing an old TR mouse driver up to date and rewriting CONFIGURE's records in place, and for adding the TR input driver `d64_dir_room`, `d64_blocks_allocatable`, `d64_create`/`d64_write`/`d64_close_write` and `d64_alloc_next`. What stays GEOS's own is the info block, the GEOS bytes of the directory entry, and moving the driver ahead of the other input drivers. | +| TR menu (`D64.ino`, through `MenuDisk.h`) | `d64_open` with the track count the extension and size give, the directory iterator for the listing, and the file reader to load a program into RAM. The listing shows what a drive's would: a deleted entry (type byte 0) is left out, and an empty slot does not end its sector. It starts at the first directory sector (18/1, or 40/3 on a D81) whatever the header links to, as the menu always has. It reads only. It opens short D64s down to 171520 bytes (some copiers write them; `Realm.d64` is one), so a sector past the file's end fails to read instead of the image being refused. | + +## Allocation: one allocator, as 1541 DOS does it + +Everything the firmware writes to an image takes its sectors from the engine's one allocator, the way a 1541, 1571 +or 1581 would: + +* A file's first sector is on the data track nearest the directory (17 on a D64 or D71, 39 on a D81), then the tracks + further out on that side, then the other side (19 up, and on a D71 the second side the same way from track 52, or on + a D81 from track 41). Each next sector is 10 sectors on from the last on the same track while it has one free. +* A track gives a sector only while its BAM count is above 0 and its bitmap shows one free; the count and the + bitmap are both brought down. +* A directory that fills takes a sector on the directory track, 3 on from its last, never the header or (on a D81) + the BAM sectors. + +The GEOS installer used to have an allocator of its own, which took the first free sector from track 1 up. GEOS does +not care where its files are, so it now uses the shared one, and the driver lands where DOS would have saved it. +`tools/geos/trmouse.py` models the same allocator, and `geos-disk-patch.test.js` holds the firmware's result to the +script's byte for byte. + +Refusals come before the first write, so a refused image is unchanged: + +| Check | Refusal | +|---|---| +| `d64_dir_check` | the header links nowhere, or the chain leaves the directory track or comes back on itself | +| `d64_dir_room`, only when no slot is free | a directory sector the BAM shows free (growing the directory could hand it out again), or no sector the directory can grow into | +| `d64_blocks_allocatable` | fewer sectors than the file and its info block need. It counts, per track, the smaller of the BAM count and the free bits: exactly what the allocator can hand out, so a BAM whose count disagrees with its bitmap is no longer refused but cannot run the write out of room partway. | + +## 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. `trblock.test.js` runs the block device on each +kind, a 40-track D64 and images with error bytes included. `geos-disk-patch.test.js` and `geos-boot.test.js` run the +GEOS installer and boot-file reader on synthetic boot disks. `menu-disk.test.js` lists directories and loads files the +way the TR menu does, from each kind, short and odd-sized images, and broken chains. 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 diff --git a/docs/GEOS64_Usage.md b/docs/GEOS64_Usage.md index 249e1b1e..0652ac09 100644 --- a/docs/GEOS64_Usage.md +++ b/docs/GEOS64_Usage.md @@ -64,7 +64,7 @@ If GEOS 64 can't start, the TeensyROM stays at the menu and shows one of these: |--|--| | GEOS 64 starts from your boot disk image on the SD card. Insert the card. | No SD card was found. | | Copy your GEOS 64 boot disk image (.D81) to /GEOS64 on the SD card, then select this again. | There is no `/GEOS64` folder, or no D81 in it holds `DESK TOP`. | -| ``: not a D64, D71 or D81 without error bytes | The file's size is not that of a disk image (a D81 is 819,200 bytes). | +| ``: not a D64, D71 or D81 image | The file's size is not that of a disk image (a D81 is 819,200 bytes, or 822,400 with error bytes). | | ``: (other reason) | The image could not be read, or its directory is damaged. | | Could not add the mouse driver to a copy of ``: the disk is full | No room for the driver. Free some space on your original image, or use another one. "the directory is full" is the same for directory entries. | | Could not add the mouse driver to a copy of ``: the SD card could not be written | The copy could not be written to the SD card. | 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. diff --git a/tools/geos/trmouse.py b/tools/geos/trmouse.py index 7293ad4d..76c8fc02 100644 --- a/tools/geos/trmouse.py +++ b/tools/geos/trmouse.py @@ -10,6 +10,10 @@ GEOS loads the first input driver in the boot disk's directory, so the driver goes into the entry ahead of every other one, and the input driver that was first moves to the slot the new file would have taken. Needs acme on PATH. + +The sectors are the ones the firmware's disk image engine (Source/Teensy/DiskImage) +picks, as 1541 DOS does: see docs/Architecture/Disk-Images.md. The firmware's +installer is tested byte for byte against this script. """ import argparse @@ -92,19 +96,36 @@ def zone_sectors(track): return 21 if track <= 17 else 19 if track <= 24 else 18 if track <= 30 else 17 +def d64_40_sectors(track): + return zone_sectors(track) if track <= 35 else 17 + + +def d64_order(): + """1541 DOS hands out data tracks nearest the directory first: 17 down to 1, then 19 up to 35.""" + return [*range(17, 0, -1), *range(19, 36)] + + +def d71_order(): + return [*d64_order(), *range(52, 35, -1), *range(54, 71)] + + +def d81_order(): + return [*range(39, 0, -1), *range(41, 81)] + + class Format: - def __init__(self, name, tracks, sectors, dir_track, dir_sectors, reserved, bam): + def __init__(self, name, tracks, sectors, dir_track, first_dir, order, bam): self.name, self.tracks, self.sectors = name, tracks, sectors - self.dir_track, self.dir_sectors = dir_track, dir_sectors - self.reserved = reserved # tracks DOS never hands out for file data - self.bam = bam # track -> (count, bitmap) as (track, sector, byte) + self.dir_track, self.first_dir = dir_track, first_dir + self.order = order # data tracks in the order DOS allocates them + self.bam = bam # track -> (count, bitmap) as (track, sector, byte) self.starts = [0] for track in range(1, tracks + 1): self.starts.append(self.starts[-1] + sectors(track)) @property - def size(self): - return self.starts[-1] * SECTOR_SIZE + def blocks(self): + return self.starts[-1] def d64_bam(track): @@ -122,22 +143,31 @@ def d81_bam(track): return (40, sector, entry), (40, sector, entry + 1) -FORMATS = {f.size: f for f in ( - Format('D64', 35, zone_sectors, 18, range(1, 19), (18,), d64_bam), - Format('D71', 70, zone_sectors, 18, range(1, 19), (18, 53), d71_bam), - Format('D81', 80, lambda track: 40, 40, range(3, 40), (40,), d81_bam), -)} +# Each kind with and without the error bytes some copiers append (one a sector, after the data) +FORMATS = {} +for f in ( + Format('D64', 35, zone_sectors, 18, 1, d64_order(), d64_bam), + Format('D64', 40, d64_40_sectors, 18, 1, d64_order(), d64_bam), + Format('D71', 70, zone_sectors, 18, 1, d71_order(), d71_bam), + Format('D81', 80, lambda track: 40, 40, 3, d81_order(), d81_bam), +): + FORMATS[f.blocks * SECTOR_SIZE] = FORMATS[f.blocks * (SECTOR_SIZE + 1)] = f + +FILE_INTERLEAVE = 10 +DIR_INTERLEAVE = 3 class Disk: + """A disk image written the way the firmware's disk image engine (Source/Teensy/DiskImage) writes it.""" + def __init__(self, image): self.format = FORMATS.get(len(image)) if self.format is None: - sizes = ', '.join(f'{f.name} {f.size}' for f in FORMATS.values()) - sys.exit(f'not a D64, D71 or D81 without error bytes ({len(image)} bytes; expected {sizes})') + sys.exit(f'not a D64, D71 or D81 image ({len(image)} bytes)') self.image = bytearray(image) if self.format.name == 'D71' and not self.image[self.offset(18, 0) + 3] & 0x80: sys.exit('this D71 is formatted single-sided') + self.chain = self.check_directory() def offset(self, track, sector): return (self.format.starts[track - 1] + sector) * SECTOR_SIZE @@ -146,74 +176,101 @@ def at(self, place): track, sector, byte = place return self.offset(track, sector) + byte - def free_sectors(self, track): - _, bitmap = (self.at(p) for p in self.format.bam(track)) - return [s for s in range(self.format.sectors(track)) - if self.image[bitmap + s // 8] & 1 << (s % 8)] - - def take(self, track): - """Mark the first free sector of a track in use; None if it has none.""" - count, bitmap = (self.at(p) for p in self.format.bam(track)) - free = self.free_sectors(track) - if self.image[count] < len(free): - sys.exit(f'the BAM count for track {track} disagrees with its bitmap') - if not free: - return None - sector = free[0] - self.image[bitmap + sector // 8] &= ~(1 << (sector % 8)) - self.image[count] -= 1 - return track, sector - - def allocate(self): - for track in range(1, self.format.tracks + 1): - if track not in self.format.reserved: - found = self.take(track) - if found: - return found - sys.exit('the disk is full') + def link(self, track, sector): + o = self.offset(track, sector) + return self.image[o], self.image[o + 1] - def dir_sectors(self): - header = self.offset(self.format.dir_track, 0) - track, sector = self.image[header], self.image[header + 1] + def check_directory(self): + """The header must link to a chain of distinct sectors on the directory track.""" + track, sector = self.link(self.format.dir_track, 0) if not track: sys.exit('the disk header links to no directory') - seen = set() + chain = [] while track: - in_directory = track == self.format.dir_track and sector in self.format.dir_sectors - if (track, sector) in seen or not in_directory: + in_directory = (track == self.format.dir_track and self.format.first_dir <= sector < self.format.sectors(track)) + if (track, sector) in chain or not in_directory: sys.exit(f'the directory chain is broken at {track}/{sector}') - seen.add((track, sector)) - yield track, sector - base = self.offset(track, sector) - track, sector = self.image[base], self.image[base + 1] + chain.append((track, sector)) + track, sector = self.link(track, sector) + return chain + + def is_free(self, track, sector): + _, bitmap = self.format.bam(track) + return bool(self.image[self.at(bitmap) + sector // 8] & 1 << (sector % 8)) + + def free_sectors(self, track): + return [s for s in range(self.format.sectors(track)) if self.is_free(track, s)] + + def count(self, track): + return self.image[self.at(self.format.bam(track)[0])] + + def take(self, track, sector): + _, bitmap = self.format.bam(track) + self.image[self.at(bitmap) + sector // 8] &= ~(1 << (sector % 8)) + self.image[self.at(self.format.bam(track)[0])] -= 1 + return track, sector + + def allocatable(self): + """Data sectors the allocator can hand out: a track gives one while its count is above 0 and a bit is free.""" + return sum(min(self.count(t), len(self.free_sectors(t))) for t in self.format.order) + + def allocate(self, after=None): + """The next data sector, as DOS takes it: after the file's last sector with an interleave of 10, else the + first free sector of the first track in DOS order that has one.""" + order = self.format.order + if after: + track, hint = after[0], after[1] + FILE_INTERLEAVE + tracks = order[order.index(track):] + else: + tracks, hint = order, 0 + for track in tracks: + if self.count(track): + n = self.format.sectors(track) + for i in range(n): + if self.is_free(track, (hint + i) % n): + return self.take(track, (hint + i) % n) + hint = 0 + sys.exit('the disk is full') def slots(self, track, sector): base = self.offset(track, sector) return [base + i * DIR_ENTRY_SIZE for i in range(SECTOR_SIZE // DIR_ENTRY_SIZE)] def dir_slots(self): - for track, sector in self.dir_sectors(): + for track, sector in self.chain: yield from self.slots(track, sector) - def grow_directory(self): - """Chain one more directory sector, as DOS does when a directory fills; returns its slots.""" - chain = list(self.dir_sectors()) - free = self.free_sectors(self.format.dir_track) - for track, sector in chain: - if sector in free: + def grow_room(self): + """Refuses a directory that could not take one more sector.""" + for track, sector in self.chain: + if self.is_free(track, sector): sys.exit(f'the BAM shows directory sector {track}/{sector} free') - new = self.take(self.format.dir_track) - if new is None or new[1] not in self.format.dir_sectors: + track = self.format.dir_track + if not self.count(track) or not any(s >= self.format.first_dir for s in self.free_sectors(track)): sys.exit('the directory is full') - o = self.offset(*chain[-1]) + + def grow_directory(self): + """Chain one more directory sector, 3 after the last one, as DOS does when a directory fills.""" + last = self.chain[-1] + track, n = self.format.dir_track, self.format.sectors(self.format.dir_track) + for i in range(n): + sector = (last[1] + DIR_INTERLEAVE + i) % n + if sector >= self.format.first_dir and self.is_free(track, sector): + break + new = self.take(track, sector) + o = self.offset(*last) self.image[o:o + 2] = bytes(new) o = self.offset(*new) self.image[o:o + SECTOR_SIZE] = padded(b'\x00\xff', SECTOR_SIZE) + self.chain.append(new) return self.slots(*new) def write_file(self, data, info): - """Write a GEOS sequential file; returns its first data sector.""" - sectors = [self.allocate() for _ in range((len(data) + 253) // 254)] + """Write a GEOS sequential file, its info block in the sector after the last data sector; returns its + first data sector, the info sector and the block count.""" + sectors = [self.allocate()] + while len(sectors) < (len(data) + 253) // 254: + sectors.append(self.allocate(sectors[-1])) for i, (track, sector) in enumerate(sectors): chunk = data[i * 254:(i + 1) * 254] if i + 1 < len(sectors): @@ -222,7 +279,7 @@ def write_file(self, data, info): link = bytes((0, len(chunk) + 1)) o = self.offset(track, sector) self.image[o:o + SECTOR_SIZE] = padded(link + chunk, SECTOR_SIZE) - info_ts = self.allocate() + info_ts = self.allocate(sectors[-1]) o = self.offset(*info_ts) self.image[o:o + SECTOR_SIZE] = info return sectors[0], info_ts, len(sectors) + 1 @@ -242,18 +299,26 @@ def info_block(size): return bytes(info) +def in_use(type_byte): + """What DOS finds by name: a file of any type, or a closed DEL.""" + return type_byte & 7 or type_byte & 0x80 + + def install(image, code): disk = Disk(image) slots = list(disk.dir_slots()) - entries = [s for s in slots if disk.image[s + 2] & 7] - if any(disk.image[s + 5:s + 21].rstrip(b'\xa0') == NAME for s in entries): + if any(in_use(disk.image[s + 2]) and disk.image[s + 5:s + 21].rstrip(b'\xa0') == NAME for s in slots): sys.exit(f'{NAME.decode()} is already on this disk') free = next((s for s in slots if disk.image[s + 2] == 0), None) + if free is None: + disk.grow_room() + if disk.allocatable() < (len(code) + 253) // 254 + 1: + sys.exit('the disk is full') + if free is None: grown = disk.grow_directory() slots += grown free = grown[0] - first, info_ts, blocks = disk.write_file(code, info_block(len(code))) now = datetime.datetime.now() entry = bytearray(30) @@ -266,8 +331,8 @@ def install(image, code): entry[28:30] = blocks.to_bytes(2, 'little') disk.image[free + 2:free + 32] = entry - drivers = [s for s in entries if disk.image[s + 24] == INPUT_TYPE] - if drivers and slots.index(drivers[0]) < slots.index(free): + drivers = [s for s in slots if disk.image[s + 2] & 7 and disk.image[s + 24] == INPUT_TYPE] + if drivers[0] != free: a, b = drivers[0], free disk.image[a + 2:a + 32], disk.image[b + 2:b + 32] = \ disk.image[b + 2:b + 32], disk.image[a + 2:a + 32]