Login

Subversion Repositories NedoOS

Rev

Rev 539 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

/*

  SjASMPlus Z80 Cross Compiler

  Copyright (c) 2004-2006 Aprisobal

  This software is provided 'as-is', without any express or implied warranty.
  In no event will the authors be held liable for any damages arising from the
  use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it freely,
  subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not claim
         that you wrote the original software. If you use this software in a product,
         an acknowledgment in the product documentation would be appreciated but is
         not required.

  2. Altered source versions must be plainly marked as such, and must not be
         misrepresented as being the original software.

  3. This notice may not be removed or altered from any source distribution.

*/


// io_trd.cpp

#include "sjdefs.h"

#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct STrdFile {
        constexpr static size_t NAME_BASE_SZ = 8;
        constexpr static size_t NAME_EXT_SZ = 1;
        constexpr static size_t NAME_ALT_EXT_SZ = 3;    // 3-letter extensions are sometimes used instead of "address" field
        constexpr static size_t NAME_FULL_SZ = NAME_BASE_SZ + NAME_EXT_SZ;
        constexpr static size_t NAME_ALT_FULL_SZ = NAME_BASE_SZ + NAME_ALT_EXT_SZ;

        byte            filename[NAME_BASE_SZ];
        byte            ext;
        byte            addressLo;              // sometimes: other two extension letters for 8.3 naming scheme
        byte            addressHi;              // can't be `word` because of BE-hosts support
        word            length;
        byte            sectorLength;
        byte            startSector;
        byte            startTrack;
}
#ifndef _MSC_VER
        __attribute__((packed));
#else
        ;
#pragma pack(pop)
#endif
static_assert(16 == sizeof(STrdFile), "TRD file header is expected to be 16 bytes long!");

#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct STrdDisc {
        constexpr static byte TRDOS_DISC_ID = 0x10;
        constexpr static size_t SECTOR_SZ = 256;
        constexpr static size_t SECTORS_PER_TRACK = 16;
        constexpr static size_t PASSWORD_SZ = 9;
        constexpr static size_t LABEL_SZ = 8;
        constexpr static byte DISK_TYPE_T80_S2 = 0x16;          // 80 tracks, double sided
        constexpr static byte DISK_TYPE_T40_S2 = 0x17;          // 40 tracks, double sided
        constexpr static byte DISK_TYPE_T80_S1 = 0x18;          // 80 tracks, single sided
        constexpr static byte DISK_TYPE_T40_S1 = 0x19;          // 40 tracks, single sided

        byte            _endOfRootDirectory             = 0x00;
        byte            _unused[224]                    = {};
        byte            freeSector                              = 0;
        byte            freeTrack                               = 1;
        byte            diskType                                = DISK_TYPE_T80_S2;
        byte            numOfFiles                              = 0;
        word            numOfFreeSectors                = (79+80)*SECTORS_PER_TRACK;    // 0x09F0 for T80_S2 empty disc
        byte            trDosId                                 = TRDOS_DISC_ID;
        byte            _unused2[2]                             = {};
        byte            password[PASSWORD_SZ]   = {' ',' ',' ',' ',' ',' ',' ',' ',' '};
        byte            _unused3[1]                             = {};
        byte            numOfDeleted                    = 0;
        byte            label[LABEL_SZ]                 = {' ',' ',' ',' ',' ',' ',' ',' '};
        byte            _unused4[3]                             = {};

        bool isTrdInfo() const {
                return (TRDOS_DISC_ID == trDosId);
        }

        static long fileOffset(const long track, const long sector) {
                return (track * SECTOR_SZ * SECTORS_PER_TRACK) + (sector * SECTOR_SZ);
        }

        void swapEndianness();
        bool writeToFile(FILE *ftrd);
}
#ifndef _MSC_VER
        __attribute__((packed));
#else
        ;
#pragma pack(pop)
#endif
static_assert(STrdDisc::SECTOR_SZ == sizeof(STrdDisc), "TRD disc info is expected to be 256 bytes long!");

void STrdDisc::swapEndianness() {
        numOfFreeSectors = sj_bswap16(numOfFreeSectors);
}

bool STrdDisc::writeToFile(FILE *ftrd) {
        if (Options::IsBigEndian) swapEndianness();             // fix endianness in binary form before write
        if (1 != fwrite(this, sizeof(STrdDisc), 1, ftrd)) return false;
        if (Options::IsBigEndian) swapEndianness();             // revert endianness back to native host form
        return true;
}

#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct STrdHead {
        constexpr static size_t NUM_OF_FILES_MAX = 128;         // 8 sectors with 16B records

        STrdFile        catalog[NUM_OF_FILES_MAX];
        STrdDisc        info;

        void swapEndianness();
        bool readFromFile(FILE *ftrd);
        bool writeToFile(FILE *ftrd);
}
#ifndef _MSC_VER
        __attribute__((packed));
#else
        ;
#pragma pack(pop)
#endif
static_assert(9 * STrdDisc::SECTOR_SZ == sizeof(STrdHead), "TRD catalog and info area should be 9 sectors long!");

void STrdHead::swapEndianness() {
        info.swapEndianness();
        for (STrdFile & file : this->catalog) file.length = sj_bswap16(file.length);
}

bool STrdHead::readFromFile(FILE *ftrd) {
        if (1 != fread(this, sizeof(STrdHead), 1, ftrd)) return false;
        if (Options::IsBigEndian) swapEndianness();
        return this->info.isTrdInfo();
}

bool STrdHead::writeToFile(FILE *ftrd) {
        if (Options::IsBigEndian) swapEndianness();             // fix endianness in binary form before write
        if (1 != fwrite(this, sizeof(STrdHead), 1, ftrd)) return false;
        if (Options::IsBigEndian) swapEndianness();             // revert endianness back to native host form
        return true;
}

/**
 * @brief Write empty TRD file (80 tracks, 2 sides) into file
 *
 * @param ff file handle to write content into
 * @param buf 4096 bytes long buffer (must be zeroed by caller) (16 sectors = 1 track)
 * @param label nullptr or 8 characters long disc label
 * @return int 1 if OK, 0 in case of write error
 */

static int saveEmptyWrite(FILE* ff, byte* buf, const char label[8]) {
        //catalog (8 zeroed sectors)
        if (8 != fwrite(buf, STrdDisc::SECTOR_SZ, 8, ff)) return 0;
        // disc info in sector 8
        {
                // the default 80 track two sided disc info initialized
                STrdDisc discInfo{};
                // replace label data if requested
                if (label) memcpy(discInfo.label, label, STrdDisc::LABEL_SZ);
                if (!discInfo.writeToFile(ff)) return 0;
        }
        // zeroes till end of first track
        if (7 != fwrite(buf, STrdDisc::SECTOR_SZ, 7, ff)) return 0;
        // remaining tracks in image contains all zeroes
        for (int i = 0; i < (79 + 80); ++i) {           // 80 tracks, two sides, one track is already done
                if (STrdDisc::SECTORS_PER_TRACK != fwrite(buf, STrdDisc::SECTOR_SZ, STrdDisc::SECTORS_PER_TRACK, ff)) return 0;
        }
        return 1;
}

bool TRD_SaveEmpty(const char* fname, const char label[8]) {
        FILE* ff;
        if (!FOPEN_ISOK(ff, fname, "wb")) {
                Error("opening file for write", fname, IF_FIRST);
                return 0;
        }
        byte* buf = (byte*) calloc(STrdDisc::SECTORS_PER_TRACK*STrdDisc::SECTOR_SZ, sizeof(byte));
        if (buf == NULL) ErrorOOM();
        int result = saveEmptyWrite(ff, buf, label);
        free(buf);
        fclose(ff);
        if (!result) Error("Write error (disk full?)", fname, IF_FIRST);
        return result;
}

ETrdFileName TRD_FileNameToBytes(const char* inputName, byte binName[12], int & nameL) {
        constexpr int baseSz = int(STrdFile::NAME_BASE_SZ);     // pre-cast to `int` (vs `nameL`)
        const char* ext = strrchr(inputName, '.');
        const int maxL = std::min(baseSz, ext ? int(ext-inputName) : baseSz);
        nameL = 0;
        while (inputName[nameL] && nameL < maxL) {
                binName[nameL] = inputName[nameL];
                ++nameL;
        }
        while (nameL < baseSz) binName[nameL++] = ' ';
        while (ext && ext[1] && nameL < int(STrdFile::NAME_ALT_FULL_SZ)) {
                binName[nameL] = ext[1];
                ++nameL;
                ++ext;
        }
        while (STrdFile::NAME_FULL_SZ != nameL && STrdFile::NAME_ALT_FULL_SZ != nameL) {
                binName[nameL++] = ' ';         // the file name is either 8+1 or 8+3 (not 8+2)
        }
        int fillIdx = nameL;
        while (fillIdx < 12) binName[fillIdx++] = 0;
        if (int(STrdFile::NAME_FULL_SZ) < nameL) return THREE_LETTER_EXTENSION;
        switch (binName[baseSz]) {
                case 'B': case 'C': case 'D': case '#':
                        return OK;
        }
        return INVALID_EXTENSION;
}

static int ReturnWithError(const char* errorText, const char* fname, FILE* fileToClose) {
        if (nullptr != fileToClose) fclose(fileToClose);
        Error(errorText, fname, IF_FIRST);
        return 0;
}

// use autostart == -1 to disable it (the valid autostart is 0..9999 as line number of BASIC program)
bool TRD_AddFile(const char* fname, const char* fhobname, int start, int length, int autostart, bool replace, bool addplace, int lengthMinusVars) {

        // do some preliminary checks with file name and autostart - prepare final catalog entry data
        union {
                STrdFile trdf;                  // structure to hold future form of catalog record about new file
                byte longFname[12];             // 12 byte access for TRD_FileNameToBytes (to avoid LGTM alert)
        };
        int Lname = 0;
        // this will overwrite also first byte of "trd.length" (12 bytes are affected, not just 11)
        const ETrdFileName nameWarning = TRD_FileNameToBytes(fhobname, longFname, Lname);
        const bool isExtensionB = ('B' == trdf.ext);
        if (!addplace) {
                if (INVALID_EXTENSION == nameWarning) {
                        WarningById(W_TRD_EXT_INVALID, fhobname);
                }
                if (THREE_LETTER_EXTENSION == nameWarning) {
                        WarningById(W_TRD_EXT_3, fhobname);
                        if (isExtensionB) {
                                WarningById(W_TRD_EXT_B, fhobname);
                                Lname = STrdFile::NAME_FULL_SZ;
                        }
                }
        }
        if (0 <= autostart && (!isExtensionB || 9999 < autostart)) {
                Warning("zx.trdimage_add_file: autostart value is BASIC program line number (0..9999) (in lua use -1 otherwise).");
                autostart = -1;
        }
        if (-1 != lengthMinusVars) {
                if (!isExtensionB) {
                        Error("zx.trdimage_add_file: length without variables is for BASIC files only.");
                        return 0;
                } else if (lengthMinusVars < 0 || length < lengthMinusVars) {
                        Error("zx.trdimage_add_file: length without variables is not in <0..length> range.");
                        return 0;
                }
        }

        // more validations - for Lua (or SAVETRD letting wrong values go through)
        if (!DeviceID) {
                Error("zx.trdimage_add_file: this function available only in real device emulation mode.");
                return 0;
        }
        if (start < 0 || 0xFFFF < start) {
                Error("zx.trdimage_add_file: start address must be in 0000..FFFF range", bp, PASS3);
                return 0;
        }
        if (length <= 0 || 0xFF00 < length) {
                // zero length not allowed any more, because TRD docs on internet are imprecise
                // and I'm not sure what is the correct way of saving zero length file => error
                Error("zx.trdimage_add_file: length must be in 0001..FF00 range", bp, PASS3);
                return 0;
        }
        if (0x10000 < start+length) {
                Error("zx.trdimage_add_file: provided start+length will run out of device memory", bp, PASS3);
                return 0;
        }
        trdf.length = word(length);
        trdf.sectorLength = byte((length + 255 + (0 <= autostart ? 4 : 0))>>8);
        if (isExtensionB) {
                trdf.addressLo = byte(length);
                trdf.addressHi = byte(length>>8);
                if (-1 != lengthMinusVars) trdf.length = word(lengthMinusVars);
        } else {
                if (Lname <= int(STrdFile::NAME_FULL_SZ)) {
                        trdf.addressLo = byte(start);   // single letter extension => "start" field is used for start value
                        trdf.addressHi = byte(start>>8);
                }
        }
        if (0 == trdf.sectorLength) {   // can overflow only when 0xFF00 length with autostart => 0
                Error("zx.trdimage_add_file: sector length over 0xFF max", bp, PASS3);
                return 0;
        }

        // read 9 sectors of disk into "trdHead" (contains root directory catalog and disk info data)
        FILE* ff;
        if (!FOPEN_ISOK(ff, fname, "r+b")) return ReturnWithError("Error opening file", fname, ff);
        STrdHead trdHead;
        if (!trdHead.readFromFile(ff)) {
                return ReturnWithError("TRD image read error", fname, ff);
        }

        // check if the requested file is already on the disk
        // in "add" or "replace" mode also delete all extra ones with the same name, keeping only last
        unsigned fileIndex = STrdHead::NUM_OF_FILES_MAX;
        for (unsigned fatIndex = 0; fatIndex < STrdHead::NUM_OF_FILES_MAX; ++fatIndex) {
                auto & entry = trdHead.catalog[fatIndex];
                if (0 == entry.filename[0]) break;              // beyond last FAT record, finish the loop
                if (memcmp(entry.filename, trdf.filename, Lname)) continue;     // different file name -> continue
                // in "add" or "replace" mode delete the previous incarnations of this filename (returns only last one)
                if ((addplace || replace) && STrdHead::NUM_OF_FILES_MAX != fileIndex) {
                        // delete the previously found file (it stays in catalog as deleted file)
                        trdHead.catalog[fileIndex].filename[0] = 1;
                        ++trdHead.info.numOfDeleted;
                }
                // remember the position of last entry with the requested file name
                fileIndex = fatIndex;
        }

        // check and process [un]found file based on the requested mode
        if (addplace) {
                // in "add" mode the file must already exist
                if (STrdHead::NUM_OF_FILES_MAX == fileIndex) {
                        return ReturnWithError("TRD image does not have a specified file to add data", fname, ff);
                }
        } else if (replace) {
                // in "replace" mode delete also the last occurance
                if (STrdHead::NUM_OF_FILES_MAX != fileIndex) {
                        auto & entry = trdHead.catalog[fileIndex];
                        if (fileIndex + 1 == trdHead.info.numOfFiles) {         // if last file in the catalog
                                // It's last file of catalog, erase it as if it was not on disc at all
                                // verify if the free space starts just where last file ends (integrity of TRD image)
                                const byte nextTrack = ((entry.sectorLength + entry.startSector) >> 4) + entry.startTrack;
                                const byte nextSector = (entry.sectorLength + entry.startSector) & 0x0F;
                                // if file connects to first free sector, salvage the space back
                                if (nextSector != trdHead.info.freeSector || nextTrack != trdHead.info.freeTrack) {
                                        return ReturnWithError("TRD free sector was not connected to last file", fname, ff);
                                }
                                // return the sectors used by file back to "free sectors" pool
                                trdHead.info.freeSector = entry.startSector;
                                trdHead.info.freeTrack = entry.startTrack;
                                trdHead.info.numOfFreeSectors += entry.sectorLength;
                                // delete the file (wipe catalog entry completely as if it was not written)
                                --trdHead.info.numOfFiles;
                                entry.filename[0] = 0;
                        } else {
                                // delete the file (but it stays in catalog as deleted file) (and new file will be added)
                                entry.filename[0] = 1;
                                ++trdHead.info.numOfDeleted;
                        }
                }
                fileIndex = trdHead.info.numOfFiles;
        } else {
                // in "normal" mode warn when file already exists
                if (STrdHead::NUM_OF_FILES_MAX != fileIndex) {
                        // to keep legacy behaviour of older sjasmplus versions, this is just warning
                        // and the same file will be added to end of directory any way
                        WarningById(W_TRD_DUPLICATE, fname);
                }
                fileIndex = trdHead.info.numOfFiles;
        }

        // fileIndex should point to valid record in catalog, verify the status and free space
        if (STrdHead::NUM_OF_FILES_MAX == fileIndex) {
                return ReturnWithError("TRD image is full of files", fname, ff);
        }
        auto & target = trdHead.catalog[fileIndex];
        const bool isNewTarget = !!memcmp(target.filename, trdf.filename, Lname);
        if (0 != target.filename[0] && isNewTarget) {
                // the target entry must have zero as first char or must have requested name
                return ReturnWithError("TRD inconsistent catalog data", fname, ff);
        }
        if (trdHead.info.numOfFreeSectors < trdf.sectorLength) {
                return ReturnWithError("TRD image has not enough free space", fname, ff);
        }
        const int keepSectors = addplace ? target.sectorLength : 0;
        if (0xFF < keepSectors + int(trdf.sectorLength)) {
                return ReturnWithError("zx.trdimage_add_file: new sector length over 0xFF max",  fname, ff);
        }

        // set the target record in catalog
        if (addplace) {
                // just add sector length, keep target.length at old value (no idea why, ask Dart Alver)
                target.sectorLength += trdf.sectorLength;
                // keeps basically EVERYTHING in the old catalog entry as it was, only sector length is raised
        } else {
                // finalize the prepared catalog entry record with starting position
                if (isNewTarget) {
                        trdf.startSector = trdHead.info.freeSector;
                        trdf.startTrack = trdHead.info.freeTrack;
                } else {
                        trdf.startSector = target.startSector;
                        trdf.startTrack = target.startTrack;
                }
                // write it to the actual catalog
                target = trdf;
        }

        // in "add" mode shift all data sectors to make room for the newly added ones
        if (addplace) {
                const long targetPos = STrdDisc::fileOffset(target.startTrack, target.startSector);
                const long oldTargetEndPos = targetPos + (keepSectors * STrdDisc::SECTOR_SZ);
                const long newTargetEndPos = targetPos + (target.sectorLength * STrdDisc::SECTOR_SZ);
                const long freePos = STrdDisc::fileOffset(trdHead.info.freeTrack, trdHead.info.freeSector);
                if (oldTargetEndPos < freePos) {        // some data after old file -> shift them a bit
                        // first move the data inside the TRD image
                        size_t dataToMoveLength = freePos - oldTargetEndPos;
                        std::unique_ptr<byte[]> dataToMove(new byte[dataToMoveLength]);
                        if (nullptr == dataToMove.get()) ErrorOOM();
                        if (fseek(ff, oldTargetEndPos, SEEK_SET)) {
                                return ReturnWithError("TRD image has wrong format", fname, ff);
                        }
                        if (dataToMoveLength != fread(dataToMove.get(), 1, dataToMoveLength, ff)) {
                                return ReturnWithError("TRD read error", fname, ff);
                        }
                        if (fseek(ff, newTargetEndPos, SEEK_SET)) {
                                return ReturnWithError("TRD image has wrong format", fname, ff);
                        }
                        // first modification of the provided TRD file (since here, if something fails, the file is damaged)
                        if (dataToMoveLength != fwrite(dataToMove.get(), 1, dataToMoveLength, ff)) {
                                return ReturnWithError("TRD write error", fname, ff);
                        }
                        dataToMove.release();
                        // adjust all catalog entries which got the content sectors shifted
                        for (unsigned entryIndex = 0; entryIndex < STrdHead::NUM_OF_FILES_MAX; ++entryIndex) {
                                auto & entry = trdHead.catalog[entryIndex];
                                if (0 == entry.filename[0]) break;              // beyond last FAT record, finish the loop
                                if (entryIndex == fileIndex) continue;  // ignore the "target" itself
                                // check if all files in catalog after target are also affected by content shift and vice versa
                                const long entryPos = STrdDisc::fileOffset(entry.startTrack, entry.startSector);
                                if ((entryIndex < fileIndex) != (entryPos < targetPos)) {
                                        return ReturnWithError("TRD inconsistent catalog data", fname, ff);
                                }
                                if (entryPos < targetPos) continue;             // this one is ahead of the target file
                                // the file got shifted content => update the catalog entry
                                entry.startTrack += (trdf.sectorLength + entry.startSector) >> 4;
                                entry.startSector = (trdf.sectorLength + entry.startSector) & 0x0F;
                        }
                } // END if (oldTargetEndPos < freePos)
        } // END if (addplace)

        // save the new data into the TRD (content sectors)
        long writePos = STrdDisc::fileOffset(target.startTrack, target.startSector);
        writePos += keepSectors * STrdDisc::SECTOR_SZ;
        if (fseek(ff, writePos, SEEK_SET)) {
                return ReturnWithError("TRD image has wrong format", fname, ff);
        }
        if (!SaveRAM(ff, start, length)) {
                return ReturnWithError("TRD write device RAM error", fname, ff);
        }
        if (!addplace && 0 <= autostart) {
                byte abin[] {0x80, 0xAA, static_cast<byte>(autostart), static_cast<byte>(autostart>>8)};
                if (4 != fwrite(abin, 1, 4, ff)) {
                        return ReturnWithError("Write error", fname, ff);
                }
        }

        // update next free sector/track position
        trdHead.info.freeTrack += (trdf.sectorLength + trdHead.info.freeSector) >> 4;
        trdHead.info.freeSector = (trdf.sectorLength + trdHead.info.freeSector) & 0x0F;
        // update remaining free sectors
        trdHead.info.numOfFreeSectors -= trdf.sectorLength;
        if (isNewTarget) ++trdHead.info.numOfFiles;

        // update whole catalog and disc info with modified data
        if (fseek(ff, 0, SEEK_SET)) {
                return ReturnWithError("TRD image has wrong format", fname, ff);
        }
        if (!trdHead.writeToFile(ff)) {
                return ReturnWithError("TRD write error", fname, ff);
        }

        fclose(ff);
        return 1;
}

int TRD_PrepareIncFile(const char* trdname, const char* filename, aint & offset, aint & length) {
        // parse filename into TRD file form (max 8+3, don't warn about 3-letter extension)
        byte trdFormName[12];
        int Lname = 0;
        TRD_FileNameToBytes(filename, trdFormName, Lname);      // ignore diagnostic info about extension

        // read 9 sectors of disk into "trdHead" (contains root directory catalog and disk info data)
        STrdHead trdHead;
        char* fullTrdName = GetPath(trdname);
        FILE* ff = SJ_fopen(fullTrdName, "rb");
        free(fullTrdName);
        fullTrdName = nullptr;
        if (nullptr == ff) return ReturnWithError("[INCTRD] Error opening file", trdname, ff);
        if (!trdHead.readFromFile(ff)) {
                return ReturnWithError("TRD image read error", trdname, ff);
        }
        fclose(ff);
        ff = nullptr;

        // find the requested file
        unsigned fileIndex = 0;
        for (fileIndex = 0; fileIndex < STrdHead::NUM_OF_FILES_MAX; ++fileIndex) {
                const auto & entry = trdHead.catalog[fileIndex];
                if (0 == entry.filename[0]) {   // beyond last FAT record, finish the loop
                        fileIndex = STrdHead::NUM_OF_FILES_MAX;
                        break;
                } else {
                        if (!memcmp(entry.filename, trdFormName, Lname)) break; // found!
                }
        }
        if (STrdHead::NUM_OF_FILES_MAX == fileIndex) {
                return ReturnWithError("[INCTRD] File not found in TRD image", filename, ff);
        }

        // calculate absolute file offset and length + validate input values
        const auto & entry = trdHead.catalog[fileIndex];
        if (INT_MAX == length) {
                length = entry.length;
                length -= offset;
        }
        const aint fileOffset = STrdDisc::fileOffset(entry.startTrack, entry.startSector);
        const aint fileEnd = fileOffset + entry.length;
        offset += fileOffset;

        // report success when resulting offset + length fits into the file definition
        if (fileOffset <= offset && (offset + length) <= fileEnd && 0 < length) return 1;

        return ReturnWithError("[INCTRD] File too short to cover requested offset and length", bp, ff);
}