Creating a New DOS in RIDE: The BS-DOS Example

Motivation

This tutorial is intended for any developer who wants to extend (privately or publicly) the Real and Imaginary Disk Editor (RIDE) with a Disk Operating System (DOS) that has not yet been implemented in the main branch. This tutorial explains in detail the general DOS-related internals of RIDE, their purposes, mutual interactions, and recommended overridings. For each step, it then immediatelly shows a case implementation of the ??? (BS-DOS), a ZX Spectrum DOS developed in Slovakia for proprietary??? MB-02 floppy drives popular in the states of former Czechoslovakia.

??? mb02 img

RIDE is an open and evolving platform. It can offer out-of-the-box solutions for a variety of purposes for both casual users who simply want to import to or export from their disks (comfortably using a File Manager tab, see below), as well as expert users for their simple forensic analyses of directory entries, boot sector values, data sector contents and errors, or track layouts. (There are much more powerfull tools for advanced analyses and data recovery!)

??? filemanager tab img

The good news is that all this functionality (like directory entries traversal or sector content analysis) becomes automatically available for any new DOS by overriding just a handful of DOS-related methods. These methods are to define the native properties of the new DOS, like for instance the structure of its directory entries, the management of its file allocation table(s), or which places on the disk to touch (and how) when importing a file. If the new DOS also features a boot sector (some obscure 8-bit DOSes don't), you will also need to override two simple methods to get the standard Boot Sector tab populated with custom DOS-specific data. As a bonus, you can extend the new DOS with a full-blown Track Map tab without any labor – it is marked "sealed" and ready for use just by one line of code (ok, two lines…). The creation of custom tabs will be covered in an individual tutorial in the future.

??? boot tab img

Before We Begin

Now onto some boring (but inevetable) formalities. Unless you plan to extend RIDE privately, you should check that your code follows a couple of simple rules before committing it to the repository.

  1. Don't reinvent the wheel. RIDE has complete code solutions for many situations (over time approaching them all) that may arise when implementing a new DOS. These solutions have been verified over time to be efficient, complete and correct, so use them instead of implementing your solution from scratch. Most of the time, we will be working with the CDos class, so this should be your first place to search for such ready-to-use solutions.
  2. Code reuse doesn't mean copying it. When you have found a complete solution, don't copy-and-paste it, but call it. This of course excludes the cases in which efficiency of your DOS would be impaired either from performance or memory-consumption point of view. In that case, copying the code and bending it for your particular needs is naturally the preferred and fully justified way.
  3. Keep simple things simple. When you implement your own solutions, don't misuse the fact that nowadays computers have virtually limitless resources. RIDE must run also on old PCs with internal floppies and very limited resources (e.g. my testing PC has 80MB of RAM). Allocate objects on stack, put items into stack-allocated buffers, use the smallest types that can hold the range of values you need to represent (char, short, etc.). Avoid copying objects – pointers and rvalue references are your friends. ???
  4. Imagine you come back to your code after a year. Use sensible??? variable and method names, don't be affraid to refactor your code even at later point of development. Methods/functions should have no other effects but those promissed in their names. Decorate variables whose values don't change after initialization with const. WRITE COMMENTS! Ask yourself whether you would understand that comment if you knew nothing about the point in solution it relates to.

But given that you are entertaining the thought of implementing a new DOS by reading this document, I guess you know all these recommendations :-)

We Are Beginning: The CDos Base Class

We start out???off not from the very beginning (like with where all DOSes are registered and how RIDE arbiters them) but shortly after that beginning, and return to the introductory (and essentially very simple) affairs later. This is mainly to get the first notion of essential knowledge neccessary to have about a DOS before trying to implement it – a sort of prerequisite check-list.

It's also worth to note that RIDE at its lowest level (where we are corrently heading for) works only with physical sector addresses. Any logical abstractions, commonly employed to facilitate the manipulation with sectors, are private properties of individual DOSes and RIDE's core doesn't care about them. From a hardware perspective, a disk is read/written by a set of heads (represented in RIDE using the THead type; heads usually move all together) that can be seeked to a particular cylinder (TCylinder type). A track (no particular type at disposal, just TTrack for private logical numbering) is in turn identified by the pair [ cylinderhead ]. A sector on a particular track is then identified by the tripple [ cylinderheadid ], where id (represented using TSectorId type) is the so called sector ID Field consisting of four Bytes [ cylindersidenumberlength_code ]. The repeated cylinder value is present to inform the controller about the cylinder (surprisingly) over which the heads are placed (they might have gone out of sync with caller's commands). The cylinder value in the track and sector identificators should thus be the same. The side value is an alias for a particular head that only a given DOS knows (but they commonly mirror the head numbers, starting from zero).

The CDos base class (in Dos.h) declares several narrow-purpose virtual methods (we for now won't care about the rest) that, if completely and correctly implemented, open all the automatic features noted in the previous section. These methods are organized into four logical groups based on the key features they target in a DOS: (1) the boot sector, if any, (2) the file allocation table, or shortly from now on just FAT, if any, (3) the filing system itself, and (4) other functionalities. The following tables summarize these methods. If you don't understand them fully at this moment or find gaps in the information provided, don't worry, they all will be revisited in depth when implementing them in later sections.

Table: Boot sector-related virtual method names and descriptions
void FlushToBootSector() const=0;
Flushes any critical work values to the boot sector. Such value is, for instance, the number of formatted cylinders, available in CDos-derivates as a direct member variable. The critical values are initialized from the supplied boot sector at the moment of image opening and are modified as the user works with the disk. A DOS should manipulate only with this way buffered critical values instead of directly modifying the boot sector.

Table: FAT-related virtual method names and descriptions
bool GetSectorStatuses(TCylinder cyl,THead head,TSector nSectors,PCSectorId bufferId,PSectorStatus buffer) const=0;
Populates the output buffer with statuses of selected sectors. Possible statuses are defined by the CDos::TSectorStatus enumeration, discussed in later sections. The method should return true if statuses of all sectors could be retrieved, otherwise false with CDos::TSectorStatus::Unavailable at place of anyhow unreachable sectors (e.g. because FAT partly unreadable).
bool ModifyStdSectorStatus(RCPhysicalAddress chs,TSectorStatus status)=0;
The complementary operation of changing one particular sector's status in the FAT. The method should return true if status could be successfully changed, otherwise false (e.g. because FAT partly unreadable).
bool GetFileFatPath(PCFile file,CFatPath &rFatPath) const=0;
Populates the output variable with sector physical addresses as they were assigned to the given file. The method should return true if the sequence of sectors could be determined from the underying FAT, no matter if the discovered FAT-path is erroneous, cyclic, or incomplete. On the other hand, it should return false if the given file doesn't exist.
DWORD GetFreeSpaceInBytes(TStdWinError &rError) const;
Deduces the amount of free space on the disk from values in the FAT. The default implementation queries individually each track about the statuses of its sectors. This may be a time-consuming operation for large volumes, hence a more efficient solution can be supplied in the overriding (e.g. FAT32 volumes contain a FS-Info sector with this value precomputed).
TCylinder GetFirstCylinderWithEmptySector() const;
Deduces from the FAT the first cylinder which contains sector that is reported empty. The default implementation queries individually each cylinder about the statuses of its sectors. This may be a time-consuming operation for large volumes, hence a more efficient solution can be supplied in the overriding (e.g. FAT32 volumes contain a FS-Info sector with this value precomputed).

Table: Filesystem-related virtual method names and descriptions
void GetFileNameOrExt(PCFile file,PTCHAR bufName,PTCHAR bufExt) const=0;
Populates the name and extension output buffers with corresponding information, usually obtained from directory entries associated with the given file. Both buffers should be at least MAX_PATH characters big. There is a couple of handy wrappers around this method which will be explained during the implementation of BS-DOS below.
TStdWinError ChangeFileNameAndExt(PFile file,LPCTSTR newName,LPCTSTR newExt,PFile &rRenamedFile)=0;
Tries to change the file's name and extension to the given values. In case of success, it should return zero and the output file variable be set to the directory entry associated with the file after the change. It should return a non-zero error indicator otherwise (e.g. because a file with the given name/extension combination already exists), and leave the original name and extension unchanged.
PTCHAR GetFileExportNameAndExt(PCFile file,bool shellCompliant,PTCHAR buf) const;
Populates the output buffer with the so called file export name and extension which may be generally different from the "native" name/extension used for the file within the open image or disk. For instance, in some 8-bit DOSes a slash "/" is a legal character in file name and/or extension. If wanting to export such file from RIDE to, for instance, Explorer, the slash must be replaced with something that's a legal character in Windows files (e.g. a Unicode hexa-form 0xNNNN). Hence whereas the "native" filename can be "Tape/Disk", the export name can be "Tape0x002FDisk" (another measure must be taken to ensure that "0x" can appear individually in the name or extension). The shellCompliant argument can suppress this behavior. Export file names and extensions are, however, forced when exchanging files between RIDE instances – a mechanism guaranteeing that the file will have the same name after import on one side as it had before the export on the other. The default implementation of this method simply calls GetFileNameOrExt, making export and native names identical.
DWORD GetFileSize(PCFile file,PBYTE pnBytesReservedBeforeData,PBYTE pnBytesReservedAfterData,TGetFileSizeOptions option) const=0;
Returns the number of Bytes the given file occupies (or zero, if queried about an invalid file). The CDos::TGetFileSizeOptions enumeration ambiguates the term "Byte occupation"; for instance, the term may be used to mean the official data length reported in associated directory entries, or the term may refer to the number of Bytes the file really occupies on the disk. Some DOSes prepend and/or append some internal data before/after the official ones – their numbers can be obtained if retrieved if non-nullptr pointers put in at corresponding places. As this method is quite often called and verbose, there is a couple of handy wrappers around it which will be explained during the implementation of BS-DOS below.
void GetFileTimeStamps(PCFile file,LPFILETIME pCreated,LPFILETIME pLastRead,LPFILETIME pLastWritten) const;
Retrieves the file stamps on file's creation, last access, and last modification, and populates with them corresponding non-nullptr output variables. The default implementation populates the buffers with recognized invalid time stamps which should also be the value for invalid files, or files which don't feature the requested stamp. Auxiliary wrappers around this method simplify retrieval of each of the time stamps.
DWORD GetAttributes(PCFile file) const=0;
Determines the attributes of the specified file and returns them mapped to corresponding Windows-defined ones (e.g. FILE_ATTRIBUTE_HIDDEN). The method should return zero ("no attributes") for an invalid file. There is a handy wrapper around this method to determine whether a file is actually a directory.
TStdWinError DeleteFile(PFile file)=0;
Carries out all steps necessary to correctly remove the file from directory currently switched to. This should be an "all or nothing" operation – either all steps are undertaken or none is. The method should return zero if it succeeds, or a non-zero error indicator otherwise.
DWORD ExportFile(PCFile file,CFile *fOut,DWORD nBytesToExportMax,LPCTSTR *pOutError) const;
Extracts the data associated with the specified file to the output stream (under the hood of CFile) and returns the number of Bytes written to the output. If the output is nullptr, then just the number of Bytes available for writing is returned (determined via one of the overloads of GetFileSize). This method is usually not needed to be overridden as the default behavior suffices – get the FAT-path of the file and write each sector in the sequence to the output, trimming the last sector to the end of the file.
TStdWinError ImportFile(CFile *fIn,DWORD fileSize,LPCTSTR nameAndExtension,DWORD winAttr,PFile &rFile)=0;
The most complex operation is always the import of a file from an external source (e.g. Windows Explorer or another instance of RIDE). The inputs to this method are (as they appear in the argument list) external input stream (under the hood of CFile), reported file size, "native" file name and extension, and attributes (mapped to Windows attributes). This method should have the transactional character of "all or nothing." In case of success, the method should return zero and the output file variable be set to the directory entry associated with the just imported file. It should return a non-zero error indicator otherwise (e.g. because unsufficient amount of free space on the disk), and leave the disk unchanged.
std::unique_ptr<TDirectoryTraversal> BeginDirectoryTraversal(PCFile directory) const=0;
Creates and returns a structure describing the current position in the traversal through the directory specified. The method should return nullptr if called for an invalid directory (e.g. a file actually isn't a directory). Auxiliary wrappers around this method facilitate the traversal through current directory and retrieval of items (files+subdirectories) in a directory.

Table: Miscellaneous virtual method names and descriptions
TStdWinError CreateUserInterface(HWND hTdi);
This method should first check whether RIDE can effectively handle the image/disk. For instance, if the FS-Info sector is corrupted in FAT32 filesystem, then the correct behavior is to warn the user on that fact and give an offer whether or not to open such image/disk. In the next step, this method should create the tabs native for the given DOS, like Track Map or File Manager. The method should return zero on success, otherwise a non-zero error indicator.
TCmdResult ProcessCommand(WORD cmd);
A method to process obtained commands. Such command is, for instance, to format the disk or to show a selected file data in the hexa-editor. On return, the method should give feedback on the success through one of the values from the CDos::TCmdResult enumeration. An indirect caller of this method is often MFC's CCmdTarget::OnCmdMsg.
bool UpdateCommandUi(WORD cmd,CCmdUI *pCmdUI) const;
A method to project the current status of the DOS into the user interface. The method should return true if the control, targeted via CCmdUI, has been affected by the state of the DOS, otherwise it should return false. An indirect caller of this method is virtually always MFC's CCmdTarget::OnCmdMsg.
void InitializeEmptyMedium(CFormatDialog::PCParameters params)=0;
After a medium has been first formatted to a requested raw capacity (e.g. 1.44 MB for a HD floppy), RIDE calls this method for the DOS to carry out its medium initialization chores. This includes, for instance, the initialization of the boot sector (if any) and/or root directory.
bool ValidateFormatChangeAndReportProblem(bool reformatting,PCFormat f) const;
During working with a medium, the user may intentionally change (or just toy with) the geometry parameters in the Boot Sector tab. After every such change, RIDE calls this method for the DOS implementation to decide whether the combination of new values comprises a legal format. This method should return true if the new geometry is a legal one, or false otherwise, in which case it also should give the user feedback on why the new format is not acceptable. It should not take the initiative and silently change the values so that they together make a legal geometry. The default implementation checks just for the basic physical sector addresses range – any soft-grained checking with respect to other information in the boot sector should be made in the overriddings.
bool CanBeShutDown(CFrameWnd *pFrame) const;
This method is called when the user attempts to close the image or disk access – an indirect caller of this method is virtually always MFC's CDocument::CanCloseFrame. The method should return true if the DOS implementation finds no reason why to not perform the closure, otherwise it should return false. The default implementation always returns true (image or disk can be closed) and it's usually not needed to override it for different behavior.

This is a lot of fuzzy information at once. The below text will fill up the gaps and provide some more details to each of the methods as they will be implemented and the resulting BS-DOS iteratively tested. For now, here is a summarization of the core types used in RIDE to represent addresses.

Table: Core addressing types
TypeUsageNote
TCylinderPhysical cylinderGuaranteed to represent cylinders from at least the range {0, …, 65535}.
THeadPhysical headGuaranteed to represent up to 256 heads, consecutively numbered 0…255.
TSideLogical headLimited to Byte range {0, …, 255}.
TTrackLogical trackGuaranteed to accommodate any logical track number from the range {0, …, 65535}; it's advised to privately use a different custom type for track number representation.
TSectorSector numberLimited to Byte range {0, …, 255}; the preferred sector-iteration variable type in cycles.
TSectorIdSector IDRepresents the so called sector ID Field.
TPhysicalAddressPhysical sectorFully specified physical sector address.

The *.MBD Image

We now do a very short detour from the DOS topic towards image containers by introducing to RIDE the native BS-DOS image, always bearing the extension *.MBD – an unstructured image containing raw sector data only. RIDE already implements such kind of images in the CImageRaw class. There is, however, no need to derive a brand new class from it – we simply will instantiate the existing CImageRaw class with custom parameters. These parameters are encapsulated in the CImage::TParameters structure declared as:

struct TProperties sealed{
    LPCTSTR name;
    TFnInstantiate fnInstantiate;
    LPCTSTR filter;
    TMedium::TType supportedMedia;
    WORD sectorLengthMin,sectorLengthMax;
};

The following table describes the members of this structure.

Table: CImage::TProperties member variables, descriptions, and values for BS-DOS.
LPCTSTR name;
The visual name of the image which the user sees in, for instance, the New image dialog.
BS-DOS case: There are two revisions of the floppy drive interface that use BS-DOS, namely MB-01 and MB-02. To indicate the supported interface revision, we assign the container the name "MB-02 image".
typedef CImage *(*TFnInstantiate)();
TFnInstantiate fnInstantiate;
A function that correctly instantiates and returns an image with current properties.
BS-DOS case: Simply MBD::__instantiate__(), see below this table.
LPCTSTR filter;
The list of file extensions the image with current properties can have. In the Open or Save as dialogs, the filter will then appear according to the template "<name> (<filter>)" where name and filter are here described properties. Extensions can be concatenated using the semicolon sign (as is usual in Windows format). All extension letters must be in lowercase.
BS-DOS case: The native BS-DOS image can take on only the *.mbd extension, hence the value is simply *.mbd. The filter will then appear as "MB-02 image (*.mbd)" in all file-related dialogs.
TMedium::TType supportedMedia;
An OR-ed set of media types the image supports, from the TMedium::TType enumeration.
BS-DOS case: The BS-DOS has been designed to support all kinds of floppies (2DD, HD, and even ED), hence the value TMedium::FLOPPY_ANY is chosen. For completeness sake let's add that RIDE currently doesn't support ED floppies as they never made it to the mainstream. However, they may be supported in the future, hence favoring TMedium::FLOPPY_ANY over OR-ed list of just 2DD and HD floppies, as in TMedium::FLOPPY_DD|TMedium::FLOPPY_HD.
WORD sectorLengthMin;
WORD sectorLengthMax;
The minimum and maximum length of each sector that can be stored in an image with current properties. The valid values should be multiples of two from the range 128…16384.
BS-DOS case: All standard sectors must be exactly 1024 Bytes long, hence both properties will take on this value.

As noted above, there is no need to derive a new class from the CImageRaw base class. Instead, the instantiation is put into a separate namespace as follows.

// in BS-DOS header file
#define BSDOS_SECTOR_LENGTH_STD 1024
namespace MBD{
    extern const CImage::TProperties Properties;
}

// in BS-DOS implementation file
namespace MBD{
    static PImage __instantiate__(){
        return new CImageRaw( &Properties, true );
    }
    const CImage::TProperties Properties={
        _T("MB-02 image"), // name
        __instantiate__, // instantiation function
        _T("*.mbd"), // filter
        TMedium::FLOPPY_ANY, // supported media
        BSDOS_SECTOR_LENGTH_STD,BSDOS_SECTOR_LENGTH_STD    // min and max sector length
    };
}

The final step in introducing the *.MBD image to RIDE is to register it. This is done by adding the image to the CImage::known list at some suitable position, for instance to keep the list alphabetically ordered. The adding is done when RIDE starts in CRideApp::InitInstance method as follows:

// in CRideApp::InitInstance method
CImage::known.AddTail( (PVOID)&CDsk5::Properties );
CImage::known.AddTail( (PVOID)&MBD::Properties );
CImage::known.AddTail( (PVOID)&CMGT::Properties );

From now on, RIDE will offer the *.MBD image in New image dialog as well as it will recognize it in Dump to destination dialog, among other places. Having the native image implemented and registered, we have prepared ourselves the baseline for implementing the BS-DOS itself, as described in the remainder of this tutorial.

Deriving CBSDOS class

The simplest way of creating a full-blown DOS that initially does nothing is to create a copy of the Unknown DOS (e.g. in Windows Explorer). RIDE turns to the Unknown DOS if it cannot recognize an image or a disk. The only duty of this DOS is to inform the user why their attempt to open an image or access a real medium has failed. The Unknown DOS is declared in DosUnknown.h as follows:

class CUnknownDos sealed:public CDos{
    const CTrackMapView trackMap;
public:
    static const TProperties Properties;

    CUnknownDos(PImage image,PCFormat pFormatBoot);

    // virtual methods inherited from CDos base class omitted for brevity
};

We copy this file along with the implementation file, DosUnknown.cpp, and add the copies, BSDOS.h and BSDOS.cpp, to RIDE's Main project.

BSDOS.* files added to VS

Instead of being derived directly from CDos, the CBSDOS class will derive from the CSpectrumDos class. This class provides its descendants with implementations of various ZX Spectrum-specific assets, including the ZX screen preview or ZX tape support, for instance. In BSDOS.cpp, we of course change that all the methods implement the CBSDOS class. The current state of the declaration file should now look like below, and the project should compile without errors.

#ifndef BSDOS_H
#define BSDOS_H

#include "MSDOS7.h"

    class CBSDOS sealed:public CSpectrumDos{
        const CTrackMapView trackMap;
    public:
        static const TProperties Properties;

        CBSDOS(PImage image,PCFormat pFormatBoot);

        // virtual methods inherited from CSpectrumDos base class omitted for brevity
    };

#endif // BSDOS_H

You have certainly noticed the usage of CTrackMapView which, as its name suggests, is the labor-free Track Map tab – we will ignore it at this moment and return to it when creating the user interface for the BS-DOS. The more important declaration is now the static Properties constant of type CDos::TProperties. An instance of this type is part of each of implemented DOSes. Put in a simple way, this constant describes the parameters (or properties) of the DOS like, for instance, its name, legal formats, or the function to correctly instantiate the DOS. RIDE peeks into this structure at various occasions – among others formatting, recognition, resetting of empty space on disk, etc. It's also the way how a new DOS introduces itself to RIDE, so we will start our implementation from here. The following table summarizes all the members of the CDos::TProperties type and their meaning, as well as explains the particular values chosen in the case of BS-DOS.

Table: CDos::TProperties member variables, descriptions, and values for BS-DOS.
LPCTSTR name;
The visual name of the DOS which the user sees in, for instance, the New image dialog.
BS-DOS case: In the case of BS-DOS, the compound name "BS-DOS ??? (MB-02)" has been chosen to indicate both the version number implemented as well as the floppy drive it targets (there are two revisions of the floppy drive interface, and a third one being currently under development).
TId id;
DOS unique identifier used, for instance, for automatic saving and restoration of dynamic properties. Make use of the MAKE_DOS_ID macro to generate a (statistically) unique identifier at compile time.
BS-DOS case: MAKE_DOS_ID('B','S','D','O','S','-','0','2')
BYTE recognitionPriority;
The number indicating "when" the DOS receives its attempt to recognize the medium during the recognition sequence – the bigger the number the sooner it will be. To choose a suitable value may seem like a sort of alchemy, but the general rule is to estimate how "bullet-proof" is the way the DOS recognizes its own media compared with other already implemented DOSes. If an unsuitable value is chosen, the user has always the chance to adjust the recognition sequence manually, and the wrongly chosen number can be fixed in the next revision.
BS-DOS case: As seen in the text below, the recognition robustness is weaker than that of MS-DOS and MDOS (in other words, BS-DOS checks fewer Bytes than both of these two), but stronger than TR-DOS. So we for the moment being chose the value 60, which lays between MDOS and TR-DOS.
typedef TStdWinError (*TFnRecognize)(PImage image,PFormat pFormatBoot);
TFnRecognize fnRecognize;

A function that performs the attempt to recognize the image or disk. The function should return zero if the recognition succeeds, or a non-zero error indicator otherwise.
BS-DOS case: The official advice of the author???s of BS-DOS is to check Bytes at offsets 0x00, 0x03, 0x20, and 0x25 in the boot sector for respective values 0x18, 0x02, 0x00, and 0x00. We set this value to CBSDOS::__recognizeDisk__ static function implemented and explained later in this tutorial.
typedef PDos (*TFnInstantiate)(PImage image,PCFormat pFormatBoot);
TFnInstantiate fnInstantiate;

A function that correctly creates and returns an instance of the DOS to be in charge over the specified image or disk, for which given geometry has been officially detected during recognition.
BS-DOS case: As with other DOSes, the function is very simple (again, with explanation of the constructor later in this tutorial):
static PDos __instantiateDos__(PImage image,PCFormat pFormatBoot){
    return new CBSDOS(image,pFormatBoot);
}
TMedium::TType supportedMedia;
An OR-ed set of media types the DOS supports, from the TMedium::TType enumeration.
BS-DOS case: The BS-DOS has been designed to support all kinds of floppies (2DD, HD, and even ED), hence the value TMedium::FLOPPY_ANY is chosen. For completeness sake let's add that RIDE currently doesn't support ED floppies as they never made it to the mainstream. However, they may be supported in the future, hence favoring TMedium::FLOPPY_ANY over OR-ed list of just 2DD and HD floppies, as in TMedium::FLOPPY_DD|TMedium::FLOPPY_HD.
CImage::PCProperties typicalImage;
The properties of the container most commonly used in combination with this DOS (e.g. *.TRD images for TR-DOS, or *.IMA for MS-DOS floppies). For instance, RIDE preselects such container in the New image dialog to facilitate the creation of a new disk.
BS-DOS case: Raw images with *.MBD extension, as we defined them earlier, are commonly used to store MB-02 disks, hence &MBD::Properties.
BYTE nStdFormats;
CFormatDialog::PCStdFormat stdFormats;

The number and list of official legal formats associated with this DOS (e.g. 720kB and 1440kB floppy formats for MS-DOS). The first format in the list is taken by RIDE as the default (e.g. in Format cylinders dialog).
BS-DOS case: The following table gives an overview of the formats most commonly used on BS-DOS floppies.
5.25" DS DD3.5"/5.25" DS DD3.5" DS HD
Cylinders408080
Sectors5511
Sector length1 kiB1 kiB1 kiB
Raw capacity400 kiB800 kiB1760 kiB
To formally describe each of these formats, RIDE provides the CFormatDialog::TStdFormat structure, declared and thrice instantiated as follows. The format named as 5.25" DS DD is the one used by RIDE as the default.
TStdFormat structure5.25" DS DD3.5"/5.25" DS DD3.5" DS HD
struct TStdFormat sealed{
    LPCSTR name;
    struct TParameters sealed{
        TCylinder cylinder0;
        struct TFormat sealed{
            TMedium::TType mediumType;
            TCylinder nCylinders;
            THead nHeads;
            TSector nSectors;
            TLengthCode sectorLengthCode;
            WORD sectorLength;
            TSector clusterSize;
        } format;
        BYTE interleaving;
        BYTE skew;
        BYTE gap3;
        BYTE nAllocationTables;
        WORD nRootDirectoryEntries;
    } params;
};
{
"5.25\" DS DD",

0,

TMedium::FLOPPY_DD,
39,
2,
5,
TLengthCode::LC_1024,
1024,
1,

1,
0,
FDD_SECTOR_GAP3_STD,
2,
32

}
{
"3.5\"/5.25\" DS DD",

0,

TMedium::FLOPPY_DD,
79,
2,
5,
TLengthCode::LC_1024,
1024,
1,

1,
0,
FDD_SECTOR_GAP3_STD,
2,
32

}
{
"3.5\" DS HD",

0,

TMedium::FLOPPY_HD,
79,
2,
11,
TLengthCode::LC_1024,
1024,
1,

1,
0,
FDD_SECTOR_GAP3_STD,
2,
32

}
TSector nSectorsOnTrackMin;
TSector nSectorsOnTrackMax;

The above defined standard formats is just a recommendation and the user is free to specify a custom number of sectors per track from range specified by these two values. RIDE at the moment doesn't allow for custom (or "unofficial") track layouts outside of this range (e.g. with nSectorsOnTrackMax+1 sectors). If specifying a custom value, it's up to the user to input a value that's suitable for the given type of medium (whereas in standard formats, it's up to the developer). If putting in a non-suitable value, formatting of a new real disk is likely to fail on the hardware level and the user be informed.
BS-DOS case: As the BS-DOS boot sector (described and implemented below) holds the actual number of sectors per track, the user is free to specify a custom value apart of just either 5 or 11. The minimum is always 1 sector per track; the maximum cannot exceed 11 for HD floppies, due to physical capacity of each track. As pointed out above, it's up to the user to input a value that's suitable for the given type of medium, so we don't need to do any special checks here.
TSector nSectorsInTotalMin;
The total minimum number of sectors that must be formatted on a new medium regardless of the geometry (for instance, 14 for MDOS floppies).
BS-DOS case: A legal BS-DOS disk must always contain a boot sector, two FATSs (each with at least one sector), a directory overview sector (the "DIRS sector"), and at last one sector for root directory entries – this implies that at least 5 healthy sectors must always be formatted on a BS-DOS disk.
BYTE nSectorsInClusterMax;
The maximum number of sectors that can be part of a single cluster (e.g. 1 for MDOS floppies, or 128 for MS-DOS floppies); this number must be a power of two, otherwise RIDE rounds this value to the nearest power of two below the value specified.
BS-DOS case: The floppies are usually formatted so that each cluster consists of a single sector, hence the value 1. The cases that a cluster consists of multiple sectors won't be discussed in this tutorial.
WORD clusterSizeMax;
The maximum possible size of a cluster in Bytes.
BS-DOS case: Given that only single-sector clusters are considered, the maximum size of a cluster is BSDOS_SECTOR_LENGTH_STD (i.e. 1024).
BYTE nAllocationTablesMin;
BYTE nAllocationTablesMax;

The minimum and maximum number of FATs in an image or on a disk (e.g. exactly 1 for MDOS, or 1…7 for MS-DOS).
BS-DOS case: The disk, for security reasons, always contains exactly two copies of a FAT, hence both properties take on the value 2.
WORD nRootDirectoryEntriesMin;
WORD nRootDirectoryEntriesMax;

The minimum and maximum number of directory entries that the root directory can accommodate. For instance, MDOS floppies have fixed number of sectors reserved for the root directory, hence both values are set to 128. Contrarily, MS-DOS root directory has flexible number of sectors, hence the number of entries varies in range 1…16384.
BS-DOS case: The root directory (as any other BS-DOS directory) has flexible number of sectors. To determine the lower limit, we lean on the fact that the size of a standard sector is fixed to 1024 and the size of a single directory entry is 32 Bytes, thus obtaining the value 32. The upper limit is given by the netto capacity of the smallest format (which is 395 kiB for 5.25" DS DD) as 12640 possible directory entries.
TSector firstSectorNumber;
Official sectors are usually numbered consecutively from a constant number (e.g. 1 for MS-DOS or MDOS disks). This property abstracts from the order in which official sectors appear on the track (interleaving and skew).
BS-DOS case: Sectors are numbered consecutively from 1.
BYTE sectorFillerByte;
BYTE directoryFillerByte;

Two different kinds of initial content Bytes – one dedicated specially for directory sectors, and one for all other sectors (e.g. MS-DOS has respective values 0xE5 and 0xF6, whereas MDOS has both values equal to 0xE5).
BS-DOS case: The value ??? is used to reset each directory sector, whereas the value ??? is used to initialize all the rest of the disk.
BYTE dataBeginOffsetInSector;
BYTE dataEndOffsetInSector;

Some obscure DOSes don't populate the whole sector length with user data, but regularly spare certain portion of each data sector for internal, to casual user inaccessible data – their numbers can be specified using these two parameters (e.g. respectively 0 and 2 for GDOS). In other words, these two numbers describe each data sector layout, and are not to be mistaken with custom data that some DOSes prepend or append to file data (e.g. each TR-DOS file with BASIC program or array is appended with extra 4 Bytes right after the data themselves – this isn't a regularity in sector data layout, nor are the extra data guaranteed to be at the end of the sector, so both of these numbers are set to 0 for TR-DOS).
BS-DOS case: All data sector length is filled with file (or directory) content, hence both of these values are set to 0.

Similarly as with the *.MBD container, we must register the new DOS by putting it into the CDos::known list at an appropriate position, for instance to keep the list alphabetically ordered. And as before, this is done when RIDE starts in the CRideApp::InitInstance method:

CDos::known.AddTail( (PVOID)&CBSDOS::Properties );
CDos::known.AddTail( (PVOID)&CGDOS::Properties );
CDos::known.AddTail( (PVOID)&CMDOS2::Properties );

The last thing we need to do before testing the actual result is to modify the constructor of the CBSDOS class to call the CSpectrumDos base class instead of CUnknownDos. This is how the constructor should look like:

CBSDOS::CBSDOS(PImage image,PCFormat pFormatBoot)
    // ctor
    // - base
    : CSpectrumDos(
        image,
        pFormatBoot,
        TTrackScheme::BY_CYLINDERS,
        &Properties,
        IDR_DOS_UNKNOWN,
        nullptr,
        TGetFileSizeOptions::OfficialDataLength
    )
    // - initialization
    , trackMap(this) {
}

The parameters put into the CSpectrumDos base class constructor are, in order as they appear, the container over which the BS-DOS should be in charge (image), the geometry and medium type stored in the container as they were recognized from the boot sector when BS-DOS got its crack on the image or a real disk in CBSDOS::__recognizeDisk__ static function (pFormatBoot), an information for all base classes on how logically consecutive tracks are layed out on the medium (TTrackScheme::BY_CYLINDERS, hinting that tracks follow one another by cylinders, and if on the same cylinder, by head; it is important only for parametrization of CDos protected helper routines, which we just did, so we can ignore this parameter for the rest of this tutorial), a pointer to the BS-DOS properties composed above (&Properties), identifier of resources associated with the BS-DOS (IDR_DOS_UNKNOWN, we leave this value for the moment being), a pointer to the File Manager tab (nullptr as we currently don't have implemented any, see a bit later in this tutorial), and finally the default way of how file size is determined (TGetFileSizeOptions::OfficialDataLength, standing for "from directory entry").

The code should currently compile without errors and run. We can assert that everything is correct by invoking the New image dialog. In it, the BS-DOS should be visible and if clicked on, a list of containers should show up with MB-02 image preselected. If we try to go on and confirm the dialog, RIDE will complain that it cannot create a new image. Similarly, no image or disk will be recognized as we don't yet have the necessary logic implemented. In the next section, we will solve for the latter by declaring the boot sector structure and writing a recognition function.

new image

The Boot Sector and Disk Recognition

The boot sector contains basic information on a disk and, in case that the disk is a system one, also the operating system bootstrap. The boot sector is always a sector with ID {0,0,1,3} on the first track, that is on Cylinder 0 and Head 0. The following table describes the meaning of its Bytes.

Table: BS-DOS (MB-02) boot sector structure, all numerical values are in Bytes, unless specified otherwise.
NameOffsetSizeDescription
BS_jmpBoot0x002Two-byte Z80 unconditional branch (jump) instruction that jumps to the start of the operating system bootstrap code. This code typically occupies the rest of the boot sector. This field must always take on the form of the JR N, where N is a Byte number from {0, …, 255}, as follows: jmpBoot[0]=0x18, jmpBoot[1]=N.
0x021Unused.
BS_signature10x031MB-02 signature Byte 0x02.
BS_nCylinders0x042Number of cylinders (usually 80 or 82).
BS_nSectors0x062Number of sectors per track (usually 5 for DD, or 11 for HD floppies).
BS_nHeads0x082Number of heads (usually 2).
BS_secPerClus0x0A2Number of sectors per allocation unit (or cluster).
BS_dirsSect160x0C2Logical number of the DIRS sector.
BS_nFatSect0x0E2Number of sectors in a single FAT copy (1 to 4).
BS_nFatBytes0x102Length in Bytes of a single FAT copy (1024*BS_nFatSect).
BS_fat1sect160x122Logical number of the first sector of the first FAT copy.
BS_fat2sect160x142Logical number of the first sector of the second FAT copy.
0x1610Unused.
BS_signature20x201MB-02 signature Byte 0x00.
BS_fmtTime0x214The date and time of formatting (in native MS-DOS bit layout and epoch).
BS_signature30x251MB-02 signature Byte 0x00.
BS_name0x2610Disk name in standard Spectrum charset.
BS_comment0x3016Personal note on this disk, not evaluated by BS-DOS.
BS_diskId0x4032Disk unique identifier consisting of random Bytes.
0x60928Unused (space for bootstrap code).

Translated into code, the boot sector is declared as the following structure. Note the TLogSector alias for a logical sector number, typedef-ed from WORD, introduced to improve readability of the code. To keep related things at the same place, the boot sector physical address and data retrieval function are also declared as a static members of the TBootSector structure.

typedef WORD TLogSector;
typedef const WORD *PCLogSector;

typedef struct TBootSector sealed{
    static const TPhysicalAddress CHS;

    static TBootSector *GetData(PImage image);

    struct{
        BYTE opCode;
        BYTE param;
    } jmpInstruction;
    BYTE reserved1;
    BYTE signature1;
    WORD nCylinders;
    WORD nSectorsPerTrack;
    WORD nHeads;
    WORD nSectorsPerCluster;
    TLogSector dirsLogSector;
    WORD nSectorsPerFat;
    WORD nBytesInFat;
    TLogSector fat1LogSector;
    TLogSector fat2LogSector;
    BYTE reserved2[10];
    BYTE signature2;
    DWORD formattedDateTime;
    BYTE signature3;
    char diskName[10];
    char diskComment[16];
    BYTE diskId[32];
    BYTE reserved3[928];
} *PBootSector;

typedef const TBootSector *PCBootSector;

Having a function that retrieves (some) critical sector data (like in this case those of the boot sector) is not always necessary, but it's benefitial because of (1) easier code management in case something changes in the implementation, and (2) security against crash. The static members are defined as follows.

const TPhysicalAddress CBSDOS::TBootSector::CHS={
    0, // Cylinder 0
    0, // Head 0
    {0,0,1,TLengthCode::LC_1024} // ID Field
};

CBSDOS::PBootSector CBSDOS::TBootSector::GetData(PImage image){
    return reinterpret_cast<PBootSector>(image->GetHealthySectorData(CHS));
}

We shall see better examples of benefits of critical sector data wrapper functions than the GetData one. Anyway this example provides a good starter in form of the GetHealthySectorData method. This method returns data of a sector at the specified physical address, or nullptr if an error occured during the reading. It is a handy wrapper around a more verbose virtual GetSectorData method that you barely will need to work with directly when implementing your own DOS. The returned pointer to raw Bytes is then cast to a pointer to TBootSector structure.

We now can approach the BS-DOS recognition. First, we must determine the type of medium (either a 2DD or HD floppy). This step is important if the container the user has accessed is actually a physical floppy drive. For regular images, like the *.MBD one, this step doesn't affect their correct behavior. The type of a medium is determined simply by attempting to explicitly set it. A successfull attempt, however, does not mean the type has been chosen correctly. The medium should at this stage be already formatted, so the correctness is confirmed by checking the number of formatted tracks on the first cylinder (Cylinder 0). A non-zero number of formatted tracks means the medium is set correctly and readable.

TStdWinError CBSDOS::__recognizeDisk__(PImage image,PFormat pFormatBoot){
    TFormat fmt={ // the minimum geometry
        TMedium::FLOPPY_DD, // first medium type
        1,1, // 1 cylinder, 1 head
        2,TLengthCode::LC_1024,BSDOS_SECTOR_LENGTH_STD, // 2 sectors, 1024 Bytes each
        1 // cluster size
    };
    if (image->SetMediumTypeAndGeometry(&fmt,StdSidesMap,BSDOS_SECTOR_NUMBER_FIRST)!=ERROR_SUCCESS
        ||
        !image->GetNumberOfFormattedSides(0)
    ){
        fmt.mediumType=TMedium::FLOPPY_HD; // next medium type, same geometry
        if (image->SetMediumTypeAndGeometry(&fmt,StdSidesMap,BSDOS_SECTOR_NUMBER_FIRST)!=ERROR_SUCCESS
            ||
            !image->GetNumberOfFormattedSides(0)
        )
            return ERROR_UNRECOGNIZED_VOLUME; // unknown Medium Type
    }
    return ERROR_CALL_NOT_IMPLEMENTED; //TODO: boot sector recognition
}

As suggested by its authors, BS-DOS is recognized by observing Bytes BS_jmpBoot[0], BS_signature1, BS_signature2, and BS_signature3, for respective values 0x18, 0x02, 0x00, and 0x00. Critical information in the boot sector cannot be tweaked or anyhow false – we therefore can also make use of the facts that (1) each track must contain at least two sectors (implies from Track 0, containing the boot and "temporary" sectors, and the fixed geometry of the native *.MBD image), but no more than 11 sectors (a HD floppy), (2) each cluster must contain at exactly one sector (as one of BS-DOS authors states, bigger cluster sizes were never used), (3) BS-DOS hasn't been used elsewhere than on floppies, so the number of sides is either 1 or 2, (4) the BS_nFatBytes field contains a 1024-multiple of BS_nFatSect, and finally (5) the first logical sector (and all subsequent) of at least one FAT must be lesser than 512. If any of these conditions isn't met, it's not a valid BS-DOS disk and the user has to eventually open it via Image → Open as command. Translated into code, the CBSDOS::__recognizeDisk__ is extended as follows. Recall that the recognition function must also instruct RIDE about the official geometry obtained from the boot sector, by populating the TFormat structure pointed to by the pFormatBoot argument, and return zero (represented by the ERROR_SUCCESS constant). On the other hand, if the boot sector is not found or doesn't contain the expected values, the function should return a sensible non-zero result representing the error (like the ERROR_UNRECOGNIZED_VOLUME constant).

TStdWinError CBSDOS::__recognizeDisk__(PImage image,PFormat pFormatBoot){
    //
    // code to determine medium type omitted for brevity
    //
    if (const PCBootSector boot=TBootSector::GetData(image))
        if (boot->jmpInstruction.opCode==0x18
            &&
            boot->signature1==0x02
            &&
            ((boot->signature2|boot->signature3)==0x00)
            &&
            2<=boot->nSectorsPerTrack && boot->nSectorsPerTrack<=11
            &&
            1<=boot->nHeads && boot->nHeads<=2
            &&
            boot->nSectorsPerCluster==1
            &&
            boot->nBytesInFat==BSDOS_SECTOR_LENGTH_STD*boot->nSectorsPerFat
            &&
            (boot->fat1LogSector<512 || boot->fat2LogSector<512)
        ){
            fmt.nCylinders=boot->nCylinders;
            fmt.nHeads=boot->nHeads;
            fmt.nSectors=boot->nSectorsPerTrack;
            fmt.clusterSize=boot->nSectorsPerCluster;
            *pFormatBoot=fmt;
            return ERROR_SUCCESS;
        }
    return ERROR_UNRECOGNIZED_VOLUME;
}

There is one more problem, a *.MBD image of a HD floppy like this one is recognized as a DD floppy. The reason is that the CImage::SetMediumTypeAndGeometry method always succeeds for a raw image like *.MBD, and the "segmentation" of the image into sectors then results in non-empty zero cylinder, so the CImage::GetNumberOfFormattedSides method returns a non-zero result (unless the image is empty). There is no other way but to manually set the type of medium based on the number of sectors: a HD floppy has more than five sectors per track, which holds for both real media and images. In all other cases, the type of the medium is left as it was recognized by the CImage::SetMediumTypeAndGeometry and CImage::GetNumberOfFormattedSides combination of methods.

TStdWinError CBSDOS::__recognizeDisk__(PImage image,PFormat pFormatBoot){
    //
    // code to determine medium type omitted for brevity
    //
    if (const PCBootSector boot=TBootSector::GetData(image))
        if (//
            // recognition conditions omitted for brevity
            //
        ){
            if (boot->nSectorsPerTrack>5)
               fmt.mediumType=TMedium::FLOPPY_HD;
            fmt.nCylinders=boot->nCylinders;
            fmt.nHeads=boot->nHeads;
            fmt.nSectors=boot->nSectorsPerTrack;
            fmt.clusterSize=boot->nSectorsPerCluster;
            *pFormatBoot=fmt;
            return ERROR_SUCCESS;
        }
    return ERROR_UNRECOGNIZED_VOLUME;
}

We can try out the result by compiling and running the project. The above refered HD image is now well recognized and open, which we can assure of by setting a breakpoint in the recognition function (e.g. at the line with the ERROR_SUCCESS result). The user interface, however, still looks the same as when inherited from the Unknown DOS. To get the immediate visual feedback for further implementation and debugging, we as next supply a new BS-DOS menu.

// UI with track-map with 1024-bytes-long sectors

Creation of BS-DOS Main Menu

The easiest way is to copy the menu of an existing Spectrum-based DOS, for instance the one of MDOS represented by the IDR_MDOS symbolic constant. We assign the new menu the IDR_BSDOS constant, remove all MDOS-specific commands and settings, and keep only those handled either by CDos or CSpectrumDos base classes. The menu should look like as follows, with all symbolic constants kept as they have been inherited from the MDOS menu.

IDR_BSDOS MENU DISCARDABLE 
BEGIN
    POPUP "BS-DOS"
    BEGIN
        MENUITEM "Fill empty space...",     ID_DOS_FILL_EMPTY_SPACE
        POPUP "Tape"
        BEGIN
            MENUITEM "Insert tape...",      ID_TAPE_OPEN
            MENUITEM "Insert blank tape...",ID_TAPE_NEW
            MENUITEM "Append tape...",      ID_TAPE_APPEND
            MENUITEM SEPARATOR
            MENUITEM "Eject\tCtrl+F4",      ID_TAPE_CLOSE
        END
        MENUITEM SEPARATOR
        MENUITEM "Export with shell-compliant names", ID_DOS_SHELLCOMPLIANTNAMES
        MENUITEM SEPARATOR
        MENUITEM "Format cylinders...\tCtrl+Shift+F", ID_DOS_FORMAT
        MENUITEM "Unformat cylinders...",   ID_DOS_UNFORMAT
        MENUITEM SEPARATOR
        MENUITEM "Show as Screen$\tCtrl+4", ID_ZX_PREVIEWASSCREEN
        MENUITEM "Show as BASIC program\tCtrl+K", ID_ZX_PREVIEWASBASIC
        MENUITEM "Show as binary\tCtrl+B",  ID_DOS_PREVIEWASBINARY
    END
END

We do the same with the accelerator table, creating a copy of IDR_MDOS, removing all MDOS-specific accelerators, and renaming the result to IDR_BSDOS.

IDR_BSDOS ACCELERATORS DISCARDABLE 
BEGIN
    "4", ID_ZX_PREVIEWASSCREEN,  VIRTKEY, CONTROL, NOINVERT
    "B", ID_DOS_PREVIEWASBINARY, VIRTKEY, CONTROL, NOINVERT
    "F", ID_DOS_FORMAT,          VIRTKEY, SHIFT, CONTROL, NOINVERT
    "K", ID_ZX_PREVIEWASBASIC,   VIRTKEY, CONTROL, NOINVERT
END

Finally, in the CBSDOS class constructor, we replace current IDR_DOS_UNKNOWN symbol with IDR_BSDOS.

CBSDOS::CBSDOS(PImage image,PCFormat pFormatBoot)
    // ctor
    // - base
    : CSpectrumDos(
        image,
        pFormatBoot,
        TTrackScheme::BY_CYLINDERS,
        &Properties,
        IDR_BSDOS,
        nullptr,
        TGetFileSizeOptions::OfficialDataLength
    )
    // - initialization
    , trackMap(this) {
}

To ensure all inherited commands are routed to the base classes, we replace the content of the CBSDOS::ProcessCommand method as follows.

CDos::TCmdResult CBSDOS::ProcessCommand(WORD cmd){
    return __super::ProcessCommand(cmd);
}

We now can test the result by compiling and running the project. The new menu appears at place where the Unknown DOS menu was shown previously. RIDE crashes for virtually any command we click on, but that's fine, we have actually just begun with the implementation.

CDos's Internal Format Representation

To keep up and running at virtually any circumstance, even if the user corrupts the boot sector while working with the disk, RIDE operates with "pre-buffered" (or "working") critical values. CDos derivates can add more, but the basic critical values are those in the TFormat structure, like the number of cylinders or the type of medium, which are initialized in CDos constructor by values recognized in the boot sector and evolve at run-time as the user works with the disk. The critical values are thus stored in the CDos::formatBoot member variable. Given that the user has no direct access to the critical values but via means provided in the user interface, their most obvious purpose is to serve as a barrier preventing the user from putting in nonsenses. It's worth to mention that despite this barrier, the values should always be tested against validity as the user could have forced the DOS via the Image → Open as command, thus bypassing the initial validation checks during recognition. If still in doubt as for this design, rest assured that buffering critical values has more benefits:

Given the last item in the list, we can continue with the implementation of BS-DOS by providing the body of the CDos::FlushToBootSector virtual method. Note how the BS_signature fields are reinitialized and the potentially wrong BS_nFatBytes value is silently recomputed.

void CBSDOS::FlushToBootSector() const{
    if (const PBootSector boot=TBootSector::GetData(image)){
        boot->signature1=0x20;
        boot->signature2 = boot->signature3 = 0x00;
        boot->nCylinders=formatBoot.nCylinders;
        boot->nSectorsPerTrack=formatBoot.nSectors;
        boot->nHeads=formatBoot.nHeads;
        boot->nSectorsPerCluster=formatBoot.clusterSize;
        boot->nBytesInFat=BSDOS_SECTOR_LENGTH_STD*boot->nSectorsPerFat;
        image->MarkSectorAsDirty(TBootSector::CHS);
    }
}

The last command, the call to CImage::MarkSectorAsDirty virtual method, informs RIDE that the sector at the specified physical address has been modified (or marked as "dirty") and that it should be saved to the disk or image at the nearest occasion of processing the Image → Save command (until then, the changes remain buffered just in the memory). If the user doesn't invoke this command, or rejects to save the changes when the application is closing, the modifications made to the sector will be lost.

The Boot Sector Tab

We will derive the Boot Sector tab (see the final result below) from the CBootView abstract class, which is in turn a derivate of CCriticalSectorView base class. Assigned to one particular sector on the disk, the CCriticalSectorView class provides the so called view (in MFC terms) on the sector's data, splitted into hexa-preview on the right handside and individual values organized into groups in a property grid on the left handside. Right out of the box, the property grid is empty but the CBootView derivate populates it with some basic information on geometry and volume label passed in through the CBootView::GetCommonBootParameters abstract method and CBootView::TCommonBootParameters structure. Information beyond this basic set must be added to the property grid manually via the CBootView::AddCustomBootParameters abstract method. This tutorial doesn't cover the internals of the property grid, however its API (in PropGrid namespace) is easy enough to understand the instrinsic usage of the component without actually knowing how the component internally works.

class CBootView:public CCriticalSectorView{
    //
    // class declaration shortened for brevity
    //
protected:
    typedef struct TCommonBootParameters sealed{
        unsigned geometryCategory:1;
            unsigned chs:1;
            unsigned sectorLength:1;
        unsigned volumeCategory:1;
            struct{
                BYTE length; // non-zero = this structure is valid
                PCHAR bufferA;
                char fillerByte;
                PropGrid::String::TOnValueConfirmedA onLabelConfirmedA;
            } label;
            struct{
                PVOID buffer; // non-Null = this structure is valid
                BYTE bufferCapacity;
            } id;
        unsigned clusterSize:1;
    } &RCommonBootParameters;

    CBootView(PDos dos,RCPhysicalAddress rChsBoot);

    virtual void GetCommonBootParameters(RCommonBootParameters rParam,PSectorData boot)=0;
    virtual void AddCustomBootParameters(HWND hPropGrid,HANDLE hGeometry,HANDLE hVolume,const TCommonBootParameters &rParam,PSectorData boot)=0;
};
// final Boot Sector tab

We begin by deriving our CBsdosBootView descendant from the CBootView base class in the scope of the CBSDOS class, giving no one outside this class the right to see and manipulate with the Boot Sector view.

class CBSDOS sealed:public CSpectrumDos{
    //
    // rest of the class declaration omitted for brevity
    //
    class CBsdosBootView sealed:public CBootView{
        void GetCommonBootParameters(RCommonBootParameters rParam,PSectorData boot) override;
        void AddCustomBootParameters(HWND hPropGrid,HANDLE hGeometry,HANDLE hVolume,
                                     const TCommonBootParameters &rParam,PSectorData boot) override;
    public:
        CBsdosBootView(CBSDOS *bsdos);
    } boot;
}

Given that the view's constructor takes some parameters, we also need to change the CBSDOS constructor that creates an instance of the Boot Sector view as follows:

CBSDOS::CBsdosBootView::CBsdosBootView(CBSDOS *bsdos)
    // ctor
    : CBootView(bsdos,TBootSector::CHS) {
}

CBSDOS::CBSDOS(PImage image,PCFormat pFormatBoot)
    // ctor
    // - base
    : CSpectrumDos(
        image,
        pFormatBoot,
        TTrackScheme::BY_CYLINDERS,
        &Properties,
        IDR_BSDOS,
        nullptr,
        TGetFileSizeOptions::OfficialDataLength
    )
    // - initialization
    , trackMap(this)
    , boot(this) {
}

To round up the formalities, we add the Boot Sector view to the user interface. The tab will be the one switched to when the DOS opens the container and the user will not be able to close the tab, unless closing the whole container. Defined in the ViewBoot.h file, note how the BOOT_SECTOR_TAB_LABEL macro is used at place where the tab caption is required, to standardize the terminology across various DOSes.

TStdWinError CBSDOS::CreateUserInterface(HWND hTdi){
    // creates DOS-specific Tabs in TDI; returns Windows standard i/o error
    CDos::CreateUserInterface(hTdi); // guaranteed to always return ERROR_SUCCESS
    CTdiCtrl::InsertTab( hTdi, 0, TRACK_MAP_TAB_LABEL, &trackMap.tab, false, nullptr, nullptr );
    CTdiCtrl::AddTabLast( hTdi, BOOT_SECTOR_TAB_LABEL, &boot.tab, true, nullptr, nullptr );
    return ERROR_SUCCESS;
}

We now can focus solely on populating the property grid. When the Boot Sector tab is switched to, the view's corresponding event handler first calls the virtual CBootView::GetCommonBootParameters, handing in the CBootView::TCommonBootParameters structure like a "questionaire" for the CBootView base class descendant to fill out. There are questions like what's the common size of DOS sectors or whether the volume has a standard ASCII label and if yes, how many characters the label can contain. The implementation of this virtual method usually doesn't take more than a couple of lines, as the hard work, the processing of the questionaire, is done by the CBootView base class. First several observations on the BS-DOS boot sector.

Put into code, the method is implemented as follows. Note that values not explicitly set in the CBootView::TCommonBootParameters structure are preset to zero by the CBootView event handler.

void CBSDOS::CBsdosBootView::GetCommonBootParameters(RCommonBootParameters rParam,PSectorData boot){
    // gets basic parameters from the Boot Sector
    rParam.geometryCategory=true; // create "Geometry" category
        rParam.chs=true; // add # of cyls, heads, sectors into "Geometry" category
                         // values are mapped to CDos' internal format representation
    rParam.volumeCategory=true; // create "Volume" category
        rParam.clusterSize=true; // add # of sectors per cluster
                                 // mapped to CDos' internal format representation
}

Once the method has finished, the CBootView event handler populates the property grid with answers from the questionaire, and this is what the intermediate state looks like.

// stav PropGridu po bez AddCustomBootParameters

As next, the CBootView event handler calls the CBootView::AddCustomBootParameters virtual method for the CBootView descendant to manually add DOS-specific values from the boot sector, not covered by the previous method. If we look at the declaration of the boot sector, the CBSDOS::TBootSector structure, we can classify the remaining information as follows.

We will begin, surprisingly, with the critical values as none of them requires a custom property grid editor – the out-of-the-box 16-bit unsigned numerical editor will suffice for all of them. First of all, however, we will create the Advanced subcategory in the Volume parent category. The latter has been created in a response to the questionaire, so the below argument hVolume contains its valid handle. The property grid's API, encapsulated in the PropGrid namespace, contains a function to create a new (sub)category, namely PropGrid::AddCategory, so we shall use it (notice the usage of the BOOT_SECTOR_ADVANCED macro that defines the standard caption for boot sector's advanced values category):

void CBSDOS::CBsdosBootView::AddCustomBootParameters(HWND hPropGrid,HANDLE hGeometry,HANDLE hVolume,const TCommonBootParameters &rParam,PSectorData _boot){
    const HANDLE hAdvanced=PropGrid::AddCategory( hPropGrid, hVolume, BOOT_SECTOR_ADVANCED );
}

Now let's populate the category. There are two variables which each item in the property grid has: the beginning of the value in memory, and the associated editor that "knows" how to modify the value (plus "knows" how to display it). In case of each critical value, its beginning will be the address in the buffered boot sector, and the associated editor will be the one instantiated by the PropGrid::Integer::DefineWordEditor static function. Each of property grid's built-in editors can be slightly "customized," so when instantiating the Word editor, we will define (through a static function CBootView::__bootSectorModified__, put in as a parameter) that modifying a value means marking the boot sector as "dirty." Particular values are then added to the property grid using the static PropGrid::AddProperty function. Put into code:

void CBSDOS::CBsdosBootView::AddCustomBootParameters(HWND hPropGrid,HANDLE hGeometry,HANDLE hVolume,const TCommonBootParameters &rParam,PSectorData _boot){
    const PBootSector boot=reinterpret_cast<PBootSector>(_boot);
    const HANDLE hAdvanced=PropGrid::AddCategory( hPropGrid, hVolume, BOOT_SECTOR_ADVANCED );
        const auto pWordEditor=PropGrid::Integer::DefineWordEditor(__bootSectorModified__);
        PropGrid::AddProperty( hPropGrid, hAdvanced, _T("FAT sectors"), &boot->nSectorsPerFat, pWordEditor );
        PropGrid::AddProperty( hPropGrid, hAdvanced, _T("FAT Bytes"), &boot->nBytesInFat, pWordEditor );
        PropGrid::AddProperty( hPropGrid, hAdvanced, _T("FAT(1) 1st sector"), &boot->fat1LogSector, pWordEditor );
        PropGrid::AddProperty( hPropGrid, hAdvanced, _T("FAT(2) 1st sector"), &boot->fat2LogSector, pWordEditor );
        PropGrid::AddProperty( hPropGrid, hAdvanced, _T("DIRS sector"), &boot->dirsLogSector, pWordEditor );
}

In contrast to critical values, all non-critical ones require custom property grid editors in which it's up to the developer to specify how long the value is, how to display it, and how to modify it. Luckily, for all three non-critical values, the required custom editors have already been implemented in the past, so we can just leverage them as follows.

Translated into code:

void CBSDOS::CBsdosBootView::AddCustomBootParameters(HWND hPropGrid,HANDLE hGeometry,HANDLE hVolume,const TCommonBootParameters &rParam,PSectorData _boot){
    const PBootSector boot=reinterpret_cast(_boot);
    PropGrid::AddProperty(
        hPropGrid, hVolume, _T("Label"), boot->diskName,
        TZxRom::CLineComposerPropGridEditor::Define( sizeof(boot->diskName), ' ', nullptr, __bootSectorModified__ )
    );
    PropGrid::AddProperty(
        hPropGrid, hVolume, _T("Comment"), &boot->diskComment,
        TZxRom::CLineComposerPropGridEditor::Define( sizeof(boot->diskComment), ' ', nullptr, __bootSectorModified__ )
    );
    PropGrid::AddProperty(
        hPropGrid, hVolume, _T("Date formatted"), &boot->formattedDateTime,
        CMSDOS7::TDateTime::DefinePropGridDateTimeEditor(__bootSectorModified__)
    );
    PropGrid::AddProperty(
        hPropGrid, hVolume, _T("Disk ID"), &boot->diskId,
        CHexaValuePropGridEditor::Define( nullptr, sizeof(boot->diskId), __bootSectorModified__ )
    );
    //
    // rest of the method omitted for brevity
}

As a rule of thumb when adding into property grid values which by design require custom editors, always search through RIDE whether an editor has already been implemented before engaging with implementing one yourself from scratch. Functions which return custom property grid editors are always prefixed with the word Define, which is also a habbit for built-in editors. Unlike with the alternative word Create, the word Define has been chosen to indicate that there's no obligation for the developer to also destroy the returned editors when they are no longer needed. The determination whether they are needed and their eventual destruction is fully left upon the property grid – just call any Define function as many times as you want and the property grid will care for freeing associated memory for you.

This essentially wraps up our implementation of the Boot Sector tab. We will yet make a second use of the property grid editors when implementing the File manager tab later, however, now onto something completely different…

Implementing the FAT

Apart of the primary purpose described by its name, the file allocation table (FAT) commonly describes the statuses of all data sectors, that is sectors where user files can be stored. The implementation of FAT in BS-DOS goes beyond that by describing the statuses of all sectors on the disk, that is including those reserved for boot data or root directory. This is effectively an implication of the fact that BS-DOS floppies have generally no fixed structure layout – the root directory can be stored literally anywhere on the disk, as is the case even for the two copies of FAT themselves! At our current implementation state, RIDE has no clue on which sectors are system ones, which are bad, or which are occupied, hence it shows all of them simply as unknown in the Track Map tab.

We begin the implementation of FAT with abstracting from physical sector addresses by introducing logical numbering of sectors. The zero-based logical number of a sector is given by the formula

LOG(Cyl,Head,SecNum) = ( Cyl * BS_nHeads + Head ) * BS_nSectors + SecNum – BSDOS_SECTOR_NUMBER_FIRST.

We encapsulate this formula into a handy CBSDOS::__fyzlog__ method, and the opposite operation (translation of a logical sector number back to a physical address) into another CBSDOS::__logfyz__ method, as follows (notice the absence of usage of the boot sector – internal format representation is employed again).

// in header file

class CBSDOS sealed:public CSpectrumDos{
    TLogSector __fyzlog__(RCPhysicalAddress chs) const;
    TPhysicalAddress __logfyz__(TLogSector ls) const;
    //
    // rest of declaration omitted for brevity
};


// in implementation file

CBSDOS::TLogSector CBSDOS::__fyzlog__(RCPhysicalAddress chs) const{
    return (chs.cylinder*formatBoot.nHeads+chs.head)*formatBoot.nSectors+chs.sectorId.sector-BSDOS_SECTOR_NUMBER_FIRST;
}
TPhysicalAddress CBSDOS::__logfyz__(TLogSector ls) const{
    const div_t A=div( ls, formatBoot.nSectors ), B=div( A.quot, formatBoot.nHeads );
    const TPhysicalAddress chs={
        B.quot,
        B.rem,
        { B.quot, sideMap[B.rem], A.rem+BSDOS_SECTOR_NUMBER_FIRST, BSDOS_SECTOR_LENGTH_STD_CODE }
    };
    return chs;
}

To spare on space, and unless inevitable, the declarations in header file will no longer be shown and it will be assumed that when implementing a method, the corresponding declaration already exists.

Another two helper methods are to get particular logical sector data, and mark particular logical sector as dirty. They are only a little more than thin wrappers around more verbose CImage virtual methods.

PSectorData CBSDOS::__getHealthyLogicalSectorData__(TLogSector logSector) const{
    return image->GetHealthySectorData( __logfyz__(logSector) );
}

void CBSDOS::__markLogicalSectorAsDirty__(TLogSector logSector) const{
    image->MarkSectorAsDirty( __logfyz__(logSector) );
}

Now we are ready to implement the reading of a FAT value. Each FAT value is structured as follows.

Table: BS-DOS (MB-02) 16-bit FAT value structure.
NameBit(s)Description
FAT_status150 = free sector, 1 = occupied or erroneous sector
FAT_cont140 = last sector in a multi-sector structure, 1 = contiguous???sector
FAT_info13–0[FAT_cont=0] = number of Bytes until the end of multi-sector structure
[FAT_cont=1] = logical number of the next sector in a multi-sector structure

Given a particular logical sector N, we first try to locate the N-th 16-bit value in the first copy of the FAT and, only if we fail, we then try to locate the N-th 16-bit value in the second copy of the FAT. In case that neither of the FAT copies is intact, we return the BSDOS_FAT_ERROR constant to inform the caller that such sector is unavailable. Note that this covers also the case when input to the below method isn't correct, e.g. it's queried the status of a sector beyond available disk space and thus beyond FAT limits. Recall that, by design, all FAT logical sector numbers are lower than 512, hence the whole FAT sector chain is described in the very first sector of the FAT itself – this is essentially one of verifications that the FAT is intact. Under the presumtion that the input to the below method is correct, another two verifications are that none of the FAT sectors is marked as officially empty and that the FAT isn't terminated preliminary, e.g. due to disk corruption. However, the opposite testing of whether FAT is excesively long isn't covered in the implementation – an excesively long FAT copy isn't a big disk inconsistency issue, as it after all might have been formatted so intentionally.

// in header file

class CBSDOS sealed:public CSpectrumDos{
    #pragma pack(1)
    typedef struct TFatValue sealed{
        enum TSpecialValues:WORD{
            FatError               =0xfffb,
            SystemSector           =0xff00,
            SectorErrorInDataField =0xfffc,
            SectorNotFound         =0xfffd,
            SectorUnavailable      =0xfffe,
            SectorUnknown          =0xffff,
            SectorEmpty            =0
        };

        WORD info:14;
        WORD continuous:1;
        WORD occupied:1;

        inline TFatValue(bool occupied,bool continuous,WORD info);
        inline TFatValue(WORD w);

        inline operator WORD() const;
    } *PFatValue;
    typedef const TFatValue *PCFatValue;
    //
    // rest of declaration omitted for brevity
};


// in implementation file

CBSDOS::TFatValue::TFatValue(bool occupied,bool continuous,WORD info)
    : info(info) , continuous(continuous) , occupied(occupied) {
}

CBSDOS::TFatValue::TFatValue(WORD w){
    *(PWORD)this=w;
}

CBSDOS::TFatValue::operator WORD() const{
    return *(PCWORD)this;
}

#define BSDOS_FAT_ITEMS_PER_SECTOR    (BSDOS_SECTOR_LENGTH_STD/sizeof(CBSDOS::TFatValue))

CBSDOS::TFatValue CBSDOS::__getLogicalSectorFatItem__(TLogSector logSector) const{
    if (const PCBootSector bootSector=boot.GetSectorData())
        for( BYTE fatCopy=0; fatCopy<2; fatCopy++ ){
            TLogSector lsFat=(&bootSector->fat1LogSector)[fatCopy];
            TLogSector index=logSector;
            if (PCFatValue fat=(PCFatValue)__getHealthyLogicalSectorData__(lsFat))
                while (lsFat<BSDOS_FAT_ITEMS_PER_SECTOR){
                    const TFatValue value=fat[lsFat];
                    if (!value.occupied)
                        break; // next FAT copy
                    if (index<BSDOS_FAT_ITEMS_PER_SECTOR)
                        // navigated to the correct FAT Sector - reading Index-th value from it
                        if ((value.continuous || index*sizeof(TFatValue)<value.info)
                            &&
                            ( fat=(PCFatValue)__getHealthyLogicalSectorData__(lsFat) )
                        )
                            return fat[index];
                        else
                            break; // next FAT copy
                    if (!value.continuous)
                        break; // next FAT copy
                    lsFat=value.info;
                    index-=BSDOS_FAT_ITEMS_PER_SECTOR;
                }
        }
    return TFatValue(TFatValue::FatError); // FAT i/o error
}

In contrast to reading, the writing to FAT means attempting to spread a particular value in both FAT copies. To inform the caller on success of the operation, a simple boolean result suffices: if true is returned, the value for the N-th sector has been successfully written to at least one FAT copy, otherwise false is returned, having an equivalent meaning as the above BSDOS_FAT_ERROR constant. Similarly as in writing, the same FAT consistency checks are employed, yielding virtually the same method structure as before. Nevertheless, what's worth pinpointing in the below implementation is the immediate marking of a modified sector as dirty using the helper method introduced earlier. Forgetting to mark a sector as dirty is quite a common error that isn't as critical on images as it is critical on real media where sectors are selectively saved back only when they have been explicitly marked as dirty (contrarily in images, the whole image is usually reconstructed and saved once at least one sector in it, even completely unrelated, has been marked as dirty, resulting in less error-prone behavior). The immediacy??? of the action is necessary as the CImage::GetTrackData and its wrappers (such as CImage::GetSectorData) are guaranteed to keep non-modified sectors in memory only until the very next call to CImage::GetTrackData. When processing the next track reading command, the container's internal memory manager may eventually decide to free up some previously buffered non-modified track(s) to keep memory consumption at leash. Marking a sector as dirty "pins" that sector in memory until it is eventually commanded to be saved back to medium.

bool CBSDOS::__setLogicalSectorFatItem__(TLogSector logSector,TFatValue newValue) const{
    bool valueWritten=false; // assumption (the Value couldn't be written into any FatCopy)
    if (const PCBootSector bootSector=boot.GetSectorData())
        for( BYTE fatCopy=0; fatCopy<2; fatCopy++ ){
            TLogSector lsFat=(&bootSector->fat1LogSector)[fatCopy];
            TLogSector index=logSector;
            if (PFatValue fat=(PFatValue)__getHealthyLogicalSectorData__(lsFat))
                while (lsFat<BSDOS_FAT_ITEMS_PER_SECTOR){
                    const TFatValue value=fat[lsFat];
                    if (!value.occupied)
                        break; // next FAT copy
                    if (index<BSDOS_FAT_ITEMS_PER_SECTOR){
                        // navigated to the correct FAT Sector - writing NewValue at Index-th position
                        if ((value.continuous || index*sizeof(TFatValue)<value.info)
                            &&
                            ( fat=(PFatValue)__getHealthyLogicalSectorData__(lsFat) )
                        ){
                            fat[index]=newValue;
                            valueWritten=true;
                            __markLogicalSectorAsDirty__(lsFat);
                        }
                        break; // next FAT copy
                    }
                    if (!value.continuous)
                        break; // next FAT copy
                    lsFat=value.info;
                    index-=BSDOS_FAT_ITEMS_PER_SECTOR;
                }
        }
    return valueWritten;
}

With this foundation set up, RIDE can finally be advised on sector statuses and reflect them in the Track Map tab by means of various colors. Apart of that visual feedback, this "unlocks" also other out-of-the-box functionalities, such as automatic computation of free disk space (used, for instance, during file import), resetting of free space to certain value (e.g., to improve compression rate of an image or to simply get rid of sensitive information), or determination of whether cylinders can be unformatted or reformatted without loss of data.

The advise is given through the CDos::GetSectorStatuses virtual method which takes as an input the information on particular track and a list of physical sector address on the track for which to retrieve statuses. The method populates the output buffer with statuses for sectors as they appear in the input list (see the below table for possible statuses), and returns true if statuses for all sectors could be retrieved, otherwise false with CDos::TSectorStatus::Unavailable at place of anyhow unreachable sectors (e.g. because FAT partly unreadable).

Table: CDos::TSectorStatus enumeration constants for sector status description.
ConstantStatus meaning
UnknownA sector whose ID doesn't match any ID from the standard format prescription, e.g. any "out of geometry" sector, typically present as part of a copy protection scheme.
SystemA sector with backbone data used by the DOS, such as typically the boot sector or FAT; in case of BS-DOS, it's also the "temporary" sector with ID={0,0,2,3}.
UnavailableA sector that is known but, for some reason, isn't included in the FAT; this is the default value for sectors whose status is stored in corrupted portion of a FAT – they are physically known but cannot be reached.
SkippedA sector that is known and available, but intentionally isn't used; such sectors are typically affiliated to deleted files on filesystems which prevent data fragmentation (e.g. Spectrum's TR-DOS).
BadA sector that is known and available, but is better to be avoided due to likeliness??? to store data incorrectly.
EmptyA sector that is known, available, and officially doesn't store any data.
OccupiedA sector that is known, available, and officially contains some data.
ReservedAnd intermediate state between empty and occupied; a sector that is known, available, affiliated to a multi-sector structure, but doesn't contain any data yet.

The mapping of BS-DOS FAT-specific values to the CDos::TSectorStatus enumeration constants is as follows:

Put into code:

bool CBSDOS::GetSectorStatuses(TCylinder cyl,THead head,TSector nSectors,PCSectorId bufferId,PSectorStatus buffer) const{
    bool result=true; // assumption (statuses of all Sectors successfully retrieved)
    for( const TLogSector logSectorBase=(formatBoot.nHeads*cyl+head)*formatBoot.nSectors-1; nSectors--; bufferId++ ){
        const TSector secNum=bufferId->sector;
        if (cyl>=formatBoot.nCylinders || head>=formatBoot.nHeads
            ||
            bufferId->cylinder!=cyl || bufferId->side!=sideMap[head] || secNum>formatBoot.nSectors
            ||
            !secNum || bufferId->lengthCode!=TLengthCode::LC_1024
        )
            // Sector number out of official Format
            *buffer++=TSectorStatus::UNKNOWN;
        else{
            // getting Sector Status from FAT
            const TLogSector ls=secNum+logSectorBase;
            if (ls<2)
                *buffer++=TSectorStatus::SYSTEM;
            else
                switch (const TFatValue fatValue=__getLogicalSectorFatItem__(ls)){
                    case TFatValue::FatError:
                        result=false;
                        //fallthrough
                    case TFatValue::SectorUnavailable:
                        *buffer++=TSectorStatus::UNAVAILABLE;
                        break;
                    case TFatValue::SystemSector:
                        *buffer++=TSectorStatus::SYSTEM;
                        break;
                    case TFatValue::SectorErrorInDataField:
                    case TFatValue::SectorNotFound:
                        *buffer++=TSectorStatus::BAD;
                        break;
                    case TFatValue::SectorUnknown:
                        *buffer++=TSectorStatus::UNKNOWN;
                        break;
                    default:
                        *buffer++ = fatValue.occupied
                                    ? TSectorStatus::OCCUPIED
                                    : TSectorStatus::EMPTY;
                        break;
                }
        }
    }
    return result;
}

To verify the method implementation, open some BS-DOS image (e.g. this???one) and switch to the Track Map tab – sectors should now be displayed using various colors reflecting their statuses. Just briefly review the whole disk, scrolling the tab content from top to the bottom and back, while searching for unknown sectors (you can also leverage the Helpers → Statistics command and check the number of unknown sectors against zero). Finding some would indicate the method doesn't cover all the angles and fixes to it are needed. There is at the moment nothing much to test more – the method "somehow" maps DOS-specific FAT values to global TSectorStatus constants, but the validity of the classification will be confirmed later when implementing BS-DOS files (and attempting to display their contents as, for instance, Spectrum screens or Basic programs).

// obarvene statusy v track map

Apart of the retrieval, RIDE may at some occasions also request the DOS to change certain values in the FAT. This rare request occurs, for instance, when formatting new or unformatting existing cylinders – in this case, RIDE takes care of the formatting/unformatting of the container and afterwards commands the DOS in charge of the container to mark sectors in corresponding tracks as either empty (when formatting) or unavailable (when unformatting). Such requests are made by means of the virtual CDos::ModifyStdSectorStatus method which takes as input parameters one particular sector's physical address (a DOS-native one, that's what the abbreviation of "standard" in the method name means), and the new status that should be associated with that sector. The method implementation is usually not as verbose as CDos::GetSectorStatuses typically is, as the range of possible new statuses is limited only to unavailable, bad, and empty. Coverage of other statuses in the CDos::ModifyStdSectorStatus method is optional, however, it's a habbit to collectively map them to unknown (if the DOS FAT supports such value) or any other DOS-specific reason why such sector should not be used anymore. In case of the BS-DOS, the implementation takes on the following form (notice the assertion in the default branch to notify the developer that this state shouldn't occur, and given that it eventually occured in the future, should be taken care of).

bool CBSDOS::ModifyStdSectorStatus(RCPhysicalAddress chs,TSectorStatus status){
    TFatValue value=TFatValue::SectorUnknown;
    switch (status){
        case TSectorStatus::UNAVAILABLE:
            value=TFatValue::SectorUnavailable;
            break;
        case TSectorStatus::BAD:
            value=TFatValue::SectorErrorInDataField;
            break;
        case TSectorStatus::EMPTY:
            value=TFatValue::SectorEmpty;
            break;
        default:
            ASSERT(FALSE);
            break;
    }
    return __setLogicalSectorFatItem__( __fyzlog__(chs), value );
}

Although we can't at the moment format a brand new BS-DOS floppy (the disk initialization method, CDos::InitializeEmptyMedium, isn't yet implemented), we can modify an existing one and thus verify the logic in the above method. Open, for simplicity reason, the same???image as above, then allow writing to it (by unchecking Image → Write protected), and switch to the Track Map tab. Click on BS-DOS → Format cylinders…. Select Expand to 80 cylinders option in the Format combo-box (Ctrl+F), and confirm the dialog, leaving all the other options to their defaults (see also the dialog snapshot below). New tracks will be added to the image and their sectors marked as empty in the FAT. For completeness sake, in case you wonder how RIDE found out the two capacities (40 and 80 cylinders) so typical for Spectrum floppies, they've been both added to the dialog options by the CSpectrumDos base class.

dlg format cylinder 76-79, rozkliknuty combo-box Format

Closely associated with the FAT are two virtual methods, CDos::GetFreeSpaceInBytes and CDos::GetFirstCylinderWithEmptySector. The former method sums up the number of empty sectors in all official tracks and multiplies the result by the standard sector size, producing thus the total free capacity on the disk in Bytes. The underlying brute force can sometimes be eliminated by storing similar information somewhere right on the disk. For instance, FAT32 volumes, if not corrupted, bear the number of count of empty clusters to speed up working with the volume. The method can then be simply overriden to read the number of such clusters and multiply it by the size of one cluster in Bytes to yield the total free capacity on the disk. Alternatively, the method can pre-compute the information up front into memory (e.g., at the moment of opening the container), and then just update it as the user works with the container. Nevertheless, in case of BS-DOS and its floppies with maximum capacity of 1.8 MB, we will settle with the default brute force. As the CBSDOS class has "inherited" the behavior from CUnknownDos class (always returning zero as the number of free Bytes), remove the overridden method, CBSDOS::GetFreeSpaceInBytes, to return to the default behavior.

The latter method, CDos::GetFirstCylinderWithEmptySector, returns the cylinder from which empty sectors should be searched for, which is an operation extensively used during file import. In its default implementation, the method returns zero, resulting in another brute force. As with CDos::GetFreeSpaceInBytes, this brute force can also be eliminated by storing similar information pre-computed somewhere aside on the disk, and the example are FAT32 volumes again. Similarly as above, we will settle with the default behavior, however, as the CBSDOS class hasn't "inherited" a modified behavior from CUnknownDos class, we can leave it untouched this time.

Dynamic Multi-sector Structures

As they don't have a fixed position on a disk, each of the two FAT copies we worked with above is actually a dynamic multi-sector structure, despite this is rather specific for BS-DOS. A much more typical representative is a file with sectors allocated dynamically as its content grows or shrinks. The sequence of sectors allocated to a file is stored in FAT and terminated with a special FAT value. The beginning of the sequence of sectors is stored in directory in a structure technically known as a directory entry. In case of BS-DOS, the directory entry is 32 Bytes long and has the following form. It's worth to note that BS-DOS has been designed to be fully compatible with Spectrum tape blocks, hence the directory entry is a hybrid of standard tape header decorated with elements common for disk files only.

Table: A BS-DOS file directory entry.
NameOffsetSizeDescription
DE_flags0x001An indication of which fields of this directory entry are valid (and whether this directory entry itself is actually valid), as follows.
Bit 7: 1 = this directory entry is valid (occupied), 0 = this directory entry is empty,
Bit 6: 1 = special file, 0 = normal file,
Bit 5: 1 = the file consists of data, 0 = the file has no data,
Bit 4: 1 = the file has a standard tape header, 0 = the file has no tape header,
Bits 3–0: reserved.
DE_created0x014The date and time of when this file was created (in native MS-DOS bit layout and epoch).
DE_header0x0517Standard Spectrum tape header.
0x162Reserved.
DE_length0x184The size of data in Bytes.
DE_flag0x1C1The flag Byte of data, as in a standard Spectrum block.
DE_attr0x1D1File attributes.
DE_firstLogSec0x1E2Logical number of the first sector allocated to this file.

A BS-DOS directory consists of entries like this and is itself a dynamic multi-sector structure as well (each directory can be allocated anywhere on the disk and can grow or shrink as it's being worked with its content). Nevertheless, the very first entry in each directory has a dedicated purpose – it's always occupied and contains information on the directory, like name or parent folder, as follows.

Table: A BS-DOS first directory entry.
NameOffsetSizeDescription
DE_flags0x001As in all other directory entries (see previous table). This field commonly contains the value 0x80.
DE_created0x014As in all other directory entries, the date and time of when this directory was created (in native MS-DOS bit layout and epoch).
DE_parent0x051The index of the parent directory which this directory is part of.
DE_name0x0610Directory name in standard Spectrum charset.
DE_comment0x1016A personalized note regarding this directory, not evaluated by BS-DOS.

The two forms that a BS-DOS directory entry can take on, can be translated to code the following way:

#pragma pack(1)
typedef struct TDirectoryEntry sealed{
    BYTE reserved:4;
    BYTE fileHasStdHeader:1;
    BYTE fileHasData:1;
    BYTE special:1;
    BYTE occupied:1;
    DWORD dateTimeCreated;
    union{
        struct{
            CTape::THeader stdHeader;
            WORD reserved;
            DWORD dataLength;
            BYTE dataFlag;
            BYTE attributes;
            TLogSector firstSector;
        } file;
        struct{
            BYTE parentDirIndex;
            char name[ZX_TAPE_FILE_NAME_LENGTH_MAX];
            char comment[16];
        } dir;
    };

    TDirectoryEntry(const CBSDOS *bsdos,TLogSector firstSector); // ctor
} *PDirectoryEntry;
typedef const TDirectoryEntry *PCDirectoryEntry;

Directory entries like this structure should always be opted as the handle (or unique identifier) of a file, passed on anywhere a variable (or input parameter) of type CDos::PFile (or its const alternative CDos::PCFile) is required. This is also the case of the last method that deals with FAT and which has intentionally been omitted in the previous section until sufficient foundation has been layed down, the virtual CDos::GetFileFatPath with the below synopsis.

class CDos{
    virtual bool GetFileFatPath(PCFile file,CFatPath &rOutFatPath) const=0;
    //...
};

The method takes as the first parameter a const-version of the file handle, CDos::PCFile, and as the second parameter a reference to a CFatPath object that receives the sequence of physical sectors that comprise the file. Taking the counter-example first, the method returns false if the handle is invalid (if the directory entry is empty, for instance). In all other cases, the method returns true, even if the sequence couldn't be retrieved without errors or couldn't be retrieved at all (e.g. both of the two copies of FAT are corrupted). In the latter case, the method sets a corresponding error through the output CDos::CFatPath object that receives the sequence. Before we continue with our implementation of BS-DOS, one warning aside: CDos::PFile is an alias for a void pointer, PVOID. Despite it may look to you as a good idea, never opt as a file handle anything else but a valid pointer. RIDE assumes it's a pointer and may internally create a local copy of the data pointed to by the pointer (and actually does so using ::memcpy). Hence casting an int zero-based index to a pointer, for instance, will quite certainly crash the application when performing certain operations (it won't crash it for each operation, and that's the trap). If you inevitably must use something else but a valid pointer (e.g. the suggested handle for a volume root directory is DOS_DIR_ROOT, defined as nullptr), make sure it doesn't step outside the DOS-specific routines, i.e. handle the specific value at particular DOS implementation level and hand over a valid pointer to any ancestor of your DOS class (e.g. to CDos or, as in our case, to CSpectrumDos).

The CDos::CFatPath class is little more than just a small wrapper around a fixed-length array of CDos::CFatPath::TItem items which describe individual sectors. The class has a simple interface consisting of self-explanatory methods: AddItem (to add a sector to the first empty slot in the underlying array), GetItems (to mediate an access to the array), and GetNumberOfItems (to retrieve the count of slots currently occupied in the array). You will rarely need the last public method GetErrorDesc (to obtain a textual representation of the error that occured during determination of the sequence of sectors), however, you will certainly want to set up the error code that occured by means of CDos::CFatPath::error direct member variable of type CDos::CFatPath::TError with the following possible values.

Table: CDos::CFatPath::TError enumeration constants for sector sequence retrieval error descriptions.
ConstantError meaning
OKSequence successfully determined without error; this is the default value which doesn't have to be set explicitly.
SectorOne of FAT sectors could not be found or was read with an error, e.g. typically data CRC error.
ValueCycleCyclic path in the FAT; this error is easily detectable by simply exceeding the number of sectors expected in the sequence (the expectation can usually be determined from values in corresponding directory entry); this error is set automatically by the CDos::CFatPath::AddItem method, thus doesn't have to be set explicitly.
ValueInvalidNonsense value in the FAT; a value out of certain range, like for instance, beyond the last sector/cluster number, etc.
ValueBadSectorValue in the FAT indicates a bad sector in a sequence. An occurence of such bad sector usually means a prelimiary termination of the sequence in FAT. If a bad-sector-value in FAT doesn't prevent the complete sequence to be detemined, then this error should not be set.
FileInvalid file to find sector sequence of, for instance, an empty directory entry, etc.

In case of BS-DOS, the CDos::GetFileFatPath method is implemented as follows.

bool CBSDOS::GetFileFatPath(PCFile file,CFatPath &rFatPath) const{
    // no FatPath can be retrieved if DirectoryEntry is Empty
    const PCDirectoryEntry de=(PCDirectoryEntry)file;
    if (!de->occupied){
        rFatPath.error=CFatPath::TError::File;
        return false;
    }
    // if File has no Sectors, we are done
    if (!de->fileHasData)
        return true;
    // otherwise, extracting the FatPath from FAT
    CFatPath::TItem item;
    const TLogSector logSectorMax=formatBoot.GetCountOfAllSectors();
    TFatValue fatValue = item.value = de->file.firstSector;
    do{
        // determining Sector's PhysicalAddress
        item.chs=__logfyz__(item.value);
        // adding the Item to the FatPath
        if (!rFatPath.AddItem(&item)) break; // also sets an error in FatPath
        // Value must "make sense"
        fatValue=__getLogicalSectorFatItem__(item.value);
        if (fatValue==TFatValue::FatError){ // if FAT Sector cannot be read
            rFatPath.error=CFatPath::TError::Sector;
            break;
        }
        if (fatValue>=TFatValue::SectorErrorInDataField){
            rFatPath.error=CFatPath::TError::ValueBadSector;
            break;
        }
        if (!fatValue.occupied
            ||
            fatValue.continuous && !(2BSDOS_SECTOR_LENGTH_STD
        ){
            rFatPath.error=CFatPath::TError::ValueInvalid;
            break;
        }
        item.value=fatValue.info;
    }while (fatValue.continuous); // until natural correct end is found
    return true;
}


CBSDOS::TDirectoryEntry::TDirectoryEntry(const CBSDOS *bsdos,TLogSector firstSector)
    : occupied(true) , fileHasStdHeader(false) , fileHasData(true) {
    file.firstSector=firstSector;
    CFatPath dummy( bsdos->formatBoot.GetCountOfAllSectors() );
    bsdos->GetFileFatPath( this, dummy );
    file.dataLength=BSDOS_SECTOR_LENGTH_STD*dummy.GetNumberOfItems();
}

Although we can't test the result at the moment, we have just "unlocked" another functionalities in RIDE, most notably the automatic export of (file) data and commands which rely on them, like directory entries hexa-preview or file content hexa-preview. The export of files themselves, e.g. as a response to user dragging them out from the File Manager tab and dropping them over a Windows Explorer, is not yet possible as the extraction of their names from directory entries is not yet implemented (the less if our implementation lacks the File Manager tab). But before creating the File Manager tab and allowing thus for the file export, we need to implement one more important thing.

Directories

In conventional DOSes like MS-DOS, a directory is implemented as a sequence of directory entries. In many DOSes, a root directory has a fixed place on the disk, spanning in constrained size across multiple immediatelly neighboring logical sectors. Many DOSes allow for subdirectories, in which case a directory entry in the "parent" serves as a pointer to the first sector (or generally, an allocation unit) of the "child," and so on recurrently. A one-way traversal through a particular directory's directory entries is realized by subclassing the CDos::TDirectoryTraversal structure, declared as follows (notice in-code explanation of individual data members):

struct TDirectoryTraversal abstract{
    const PCFile directory;   // directory to traverse
    const WORD entrySize;     // directory entry size
    TPhysicalAddress chs;     // currently traversed sector
    union{
        PFile entry;          // current directory entry
        TStdWinError warning; // up to the caller to take appropriate action
    };
    TDirEntryType entryType;  // type of content expressed by the current directory entry

    TDirectoryTraversal(PCFile directory,WORD entrySize);

    virtual PFile AllocateNewEntry();
    virtual bool AdvanceToNextEntry()=0;
    virtual void ResetCurrentEntry(BYTE directoryFillerByte) const=0;
    PFile GetNextFileOrSubdir();
};

Although bearing self-explanatory names, the associated methods deserve a more detailed explanation.

The TDirEntryType, describing the type of current directory entry, is an enumeration with the following symbolic constants.

Table: CDos::TDirectoryTraversal::TDirEntryType enumeration constants to describe directory entry content.
ConstantMeaning
EmptyThe current directory entry could be read and is empty, available for re-use.
FileThe current directory entry could be read and is occupied by a file.
SubDirThe current directory entry could be read and is occupied by a subdirectory.
CustomThe current directory entry could be read and is occupied by a content that only specific DOS understands and can process. Such entries are ignored by all base classes like, for instance, CDos or CFileManagerView.
WarningAn issue occured while retrieving the current directory entry, typically a sector has not been found or was read with error. Whether this is a problem as critical as to stop the traversal is up to the caller to decide, nevertheless, continuing to traverse the directory usually suffices (as virtually in all CDos routines that we will indirectly call below).
EndEnd of directory has been reached and no more directory entries can be retrieved by subsequent calls to AdvanceToNextEntry. Although this is an optional constant, new code should use it to explicitly indicate to the caller the end of directory.

A TDirectoryTraversal-based traversal object is created and returned by the CDos::BeginDirectoryTraversal method with the following synopsis, taking as the single input parameter a handle of the directory to traverse (or DOS_DIR_ROOT to indicate the root directory). The method should return nullptr if passed in an invalid directory handle.

class CDos{
    std::unique_ptr<TDirectoryTraversal> BeginDirectoryTraversal(PCFile directory) const=0;
};

Now, let's implement the directory traversal for BS-DOS. Its structure of directories doesn't resemble a general graph, nor a tree, nor a forest. The best expression for it is a greengrass??? – like in real world, each ???steblo of ???travy (a subdirectory without possibility of its own subdirectories) has a common ground (the root directory). The catch is that the size of root directory entries is different from the size of subdirectory entries, so we will have to cover each possibility separately by its own TDirectoryTraversal-derived traversal object.

We begin with the root directory traversal. BS-DOS can contain up to 256 subdirectories, the entries of all of which are stored in a single sector technically refered to simply as DIRS. The logical number of the DIRS sector is stored in the boot sector in BS_dirsSect16 field. The root directory entries (or "slots" for subdirectories) take on the following form.

Table: The BS-DOS root directory entry (or a "slot" for a subdirectory) structure.
NameOffsetSizeDescription
DIRS_flags0x001An indication whether the subdirectory exists in Bit 7. The remaining lower bits are unused.
DIRS_checksum0x011A consistency checksum of the subdirectory name, implemented as a plain XOR of all name characters (including blank ones).
DIRS_firstSect0x022First subdirectory logical sector number stored in lower 14 bits. The remaining two upper bits are unused.

The DIRS sector will be encapsulated in a CDirsSector class, facilitating access to and operations with the slots.

class CBSDOS sealed:public CSpectrumDos
{
    class CDirsSector sealed{
        const CBSDOS *const bsdos;
    public:
        #pragma pack(1)
        typedef struct TSlot sealed{
            BYTE reserved1:7;
            BYTE subdirExists:1;
            BYTE nameChecksum;
            TLogSector firstSector:14;
            TLogSector reserved2:2;
        } *PSlot;
        typedef const TSlot *PCSlot;

        CDirsSector(const CBSDOS *bsdos);

        PSlot GetSlots() const;
        void MarkAsDirty() const;
    } dirsSector;

    // rest of declaration omitted for brevity
};

The GetSlots method will read the DIRS sector into memory and, if successfull, return pointer to the first slot (or nullptr in case of any read error). The MarkAsDirty method will be just a little more than a wrapper around CBSDOS::__markLogicalSectorAsDirty__, as the following code snippet shows.

CBSDOS::CBSDOS(PImage image,PCFormat pFormatBoot)
    // ctor
    : CSpectrumDos(
        image,
        pFormatBoot,
        TTrackScheme::BY_CYLINDERS,
        &Properties,
        IDR_BSDOS,
        nullptr,
        TGetFileSizeOptions::OfficialDataLength
    )
    , trackMap(this)
    , boot(this)
    , dirsSector(this) {
}


CBSDOS::CDirsSector::CDirsSector(const CBSDOS *bsdos)
    // ctor
    : bsdos(bsdos) {
}

CBSDOS::CDirsSector::PSlot CBSDOS::CDirsSector::GetSlots() const{
    if (const PCBootSector bootSector=bsdos->boot.GetSectorData())
        if (const PSlot slot=(PSlot)bsdos->__getHealthyLogicalSectorData__(bootSector->dirsLogSector))
            return slot;
    return nullptr;
}

void CBSDOS::CDirsSector::MarkAsDirty() const{
    if (const PCBootSector bootSector=bsdos->boot.GetSectorData())
        bsdos->__markLogicalSectorAsDirty__(bootSector->dirsLogSector);
}

With this foundation set, the traversal object class can be easily implemented. In the object constructor, the CDirsSector::GetSlots method is called to retrieve the data of the DIRS sector. If the sector is found, a pointer is established so that it points before the first slot, and a counter of remaining slots is initialized to its maximum. With each call to TDirectoryTraversal::AdvanceToNextEntry, the pointer is advanced by one slot and the counter is decremented. As it's traversed through the root directory, each slot is checked whether it's either empty or contains a reference to a subdirectory, and corresponding state is selected for the object. On the other hand, if the DIRS sector is not found, the object state is set to TDirEntryType::Warning in which it remains during the whole traversal. Finally, when the counter reaches zero, the object moves to the final TDirEntryType::End state in which it already remains for any subsequent calls to TDirectoryTraversal::AdvanceToNextEntry. The following code implements this idea.

// in header file

class CBSDOS sealed:public CSpectrumDos
{
    class CDirsSector sealed{
    public:
        class CTraversal sealed:public TDirectoryTraversal{
            WORD nSlotsRemaining;
        public:
            CTraversal(const CBSDOS *bsdos);

            bool AdvanceToNextEntry() override;
            void ResetCurrentEntry(BYTE directoryFillerByte) const override;
        };

        // rest of both declarations omitted for brevity
    } dirsSector;
};


// in implementation file

CBSDOS::CDirsSector::CTraversal::CTraversal(const CBSDOS *bsdos)
    // ctor
    : TDirectoryTraversal( DOS_DIR_ROOT, sizeof(TSlot) )
    , nSlotsRemaining(256) {
    if (entry=bsdos->dirsSector.GetSlots()){
        chs=bsdos->__logfyz__( bsdos->boot.GetSectorData()->dirsLogSector );
        entry=((PSlot)entry)-1;
    }
}

bool CBSDOS::CDirsSector::CTraversal::AdvanceToNextEntry(){
    if (!nSlotsRemaining){ // all Slots traversed
        entryType=TDirectoryTraversal::END;
        return false;
    }if (!entry){ // DIRS Sector not found
        entryType=TDirectoryTraversal::WARNING, warning=ERROR_SECTOR_NOT_FOUND;
        nSlotsRemaining--;
        return true;
    }
    entryType= ((PSlot)( entry=((PSlot)entry)+1 ))->subdirExists
               ? TDirectoryTraversal::SUBDIR
               : TDirectoryTraversal::EMPTY;
    nSlotsRemaining--;
    return true;
}

void CBSDOS::CDirsSector::CTraversal::ResetCurrentEntry(BYTE directoryFillerByte) const{
    //nop (can't reset a root directory slot)
}

The traversal through "standard" directory entries (those which already can contain files, like the TDirectoryEntry structure from above) is at first sight more complicated, however unlike the root directory traversal which has been rather special for BS-DOS, it has a standardized recommended pattern which you can find in any of implemented DOSes, and which you should stick to if wanting to facilitate orientation in your code to other programmers (actually, the above CDirsSector::CTraversal implements a special case of this pattern).

The following implementation of this pattern should resolve any ambiguities in its description. Notice the usage of a CFatPath member variable to retrieve the underlying sequence of sectors affilliated to the specified directory. Notice also the mapping of the first directory entry to the TDirEntryType::Custom state to indicate to any base class that it should ignore this entry.

// in header file

#pragma pack(1)
class CBSDOS sealed:public CSpectrumDos
{
    struct TDirectoryEntry sealed{
        class CTraversal sealed:public TDirectoryTraversal{
            const CBSDOS *const bsdos;
            const CFatPath dirFatPath;
            TLogSector nDirSectorsTraversed;
            WORD nRemainingEntriesInSector;
        public:
            CTraversal(const CBSDOS *bsdos,PCFile slot);

            bool AdvanceToNextEntry() override;
            void ResetCurrentEntry(BYTE directoryFillerByte) const override;
            PFile AllocateNewEntry() override;
        };

        // rest of both declarations omitted for brevity
    };
};


// in implementation file

CBSDOS::TDirectoryEntry::CTraversal::CTraversal(const CBSDOS *bsdos,PCFile pSlot)
    : TDirectoryTraversal( pSlot, sizeof(TDirectoryEntry) )
    , bsdos(bsdos)
    , dirFatPath( bsdos, &TDirectoryEntry(bsdos,((CDirsSector::PCSlot)slot)->firstSector) )
    , nDirSectorsTraversed(0)
    , nRemainingEntriesInSector(0) {
}

#define DIR_ENTRIES_PER_SECTOR    (BSDOS_SECTOR_LENGTH_STD/sizeof(TDirectoryEntry))

bool CBSDOS::TDirectoryEntry::CTraversal::AdvanceToNextEntry(){
    // getting the next LogicalSector with Directory
    const bool isDirNameEntry=(nDirSectorsTraversed|nRemainingEntriesInSector)==0;
    if (!nRemainingEntriesInSector){
        CFatPath::PCItem items; DWORD nItems;
        dirFatPath.GetItems(items,nItems);
        if (nDirSectorsTraversed==nItems){ // end of Directory or FatPath invalid
            entryType=TDirectoryTraversal::End;
            return false;
        }
        entry=bsdos->image->GetHealthySectorData( chs=items[nDirSectorsTraversed++].chs );
        if (!entry){ // Sector not found
            entryType=TDirectoryTraversal::Warning, warning=ERROR_SECTOR_NOT_FOUND;
            return true;
        }else
            entry=(PDirectoryEntry)entry-1; // pointer set "before" the first DirectoryEntry
        nRemainingEntriesInSector=DIR_ENTRIES_PER_SECTOR;
    }
    // getting the next DirectoryEntry
    entry=(PDirectoryEntry)entry+1;
    if (isDirNameEntry)
        entryType=TDirectoryTraversal::Custom;
    else
        entryType= ((PDirectoryEntry)entry)->occupied
                   ? TDirectoryTraversal::File
                   : TDirectoryTraversal::Empty;
    nRemainingEntriesInSector--;
    return true;
}

void CBSDOS::TDirectoryEntry::CTraversal::ResetCurrentEntry(BYTE directoryFillerByte) const{
    if (entry)
        (PDirectoryEntry)( ::memset(entry,directoryFillerByte,entrySize) )->occupied=false;
}

The implementation of the TDirectoryTraversal::AllocateNewEntry, omitted in the above listing, deserves little more explanation. In principle, we need to find an empty healthy sector on the disk, affilliate it to the directory in question, initialize all of its directory entries to the "empty" state, and return a pointer to the first directory entry in it. The most tricky part in this workload is to find an empty sector on the disk. Technically, this and similar problems are easily feasible by importing a single Byte to the disk using the CDos::__importFileData__ method. We will revisit this base class method later when importing real files from Windows Explorer by dropping them over the File Manager tab, however, at the moment being it suffices just to mention that instead of data provided from MFC's wrappers around COM, we will provide data using wrapper around a single-Byte buffer. On its termination, the CDos::__importFileData__ method populates a provided CFatPath object with a sector sequence that contains the data, and returns an error code indicating the result of the operation. The following method calls CDos::__importFileData__ and extracts from the CFatPath object the sector to which the single Byte has been imported. On failure, the method returns zero (the logical number of the boot sector).

CBSDOS::TLogSector CBSDOS::__getFirstHealthyFreeSector__(){
    BYTE buf; // single-Byte buffer (value is irrelevant)
    PFile p; // the handle representing the imported single-Byte file
    CDirsSector::TSlot emptySlot={}; // eventually used for root Directory
    TDirectoryEntry emptyDe(this,0); // eventually used for root Subdirectory
    CFatPath emptySector(this,sizeof(buf));
    if (__importFileData__(
            &CMemFile(&buf,sizeof(buf)), // wrapper around the buffer
            currentDir==DOS_DIR_ROOT ? (PFile)&emptySlot : (PFile)&emptyDe,
            _T(""), _T(""), // imported file name and extension (irrelevant)
            sizeof(buf), // length of data to import
            p, // imported file handle
            emptySector // an object to receive the single empty healthy sector
        )!=ERROR_SUCCESS
    )
        return 0; // import has failed
    DeleteFile(p); // deleting the imported auxiliary single-Byte File
    CFatPath::PCItem pItem; DWORD n;
    emptySector.GetItems(pItem,n);
    return __fyzlog__(pItem->chs);
}

With this method, the task of extending the sequence of directory sectors with one extra sector can be already accomplished as follows.

CDos::PFile CBSDOS::TDirectoryEntry::CTraversal::AllocateNewEntry(){
    // reusing the first empty DirectoryEntry from current position
    while (AdvanceToNextEntry())
        if (entry && !((PDirectoryEntry)entry)->occupied)
            return entry;
    // allocating a new Directory Sector and returning the first DirectoryEntry in it
    const TLogSector newLogSector=const_cast(bsdos)->__getFirstHealthyFreeSector__();
    if (newLogSector<2) // new healthy Sector couldn't be allocated
        return nullptr;
    CFatPath::PCItem pItem; DWORD n;
    dirFatPath.GetItems(pItem,n);
    if (bsdos->__setLogicalSectorFatItem__( // may fail if the last Sector in Directory is Bad
            bsdos->__fyzlog__(pItem[n-1].chs),
            TFatValue( true, true, newLogSector )
        )
    )
        return nullptr;
    bsdos->__setLogicalSectorFatItem__( // guaranteed to always succeed, given an Empty Sector has been found
        newLogSector,
        TFatValue( true, false, BSDOS_SECTOR_LENGTH_STD )
    );
    return ::ZeroMemory( // resetting all DirectoryEntries
               bsdos->__getHealthyLogicalSectorData__(newLogSector),
               BSDOS_SECTOR_LENGTH_STD
           );
}

To round this section up, we implement the virtual CDos::BeginDirectoryTraversal method that creates and returns a directory traversal object for the directory specified in the input parameter. If an input handle regards the root directory, we instantiate and return a root directory traversal object. Contrarily, if the input handle regards a subdirectory, we instantiate and return a subdirectory traversal object.

std::unique_ptr<CDos::TDirectoryTraversal> CBSDOS::BeginDirectoryTraversal(PCFile directory) const{
    if (directory==ZX_DIR_ROOT)
        return std::unique_ptr<TDirectoryTraversal>( new CDirsSector::CTraversal(this) );
    else
        return std::unique_ptr<TDirectoryTraversal>( new TDirectoryEntry::CTraversal(this,directory) );
}

Congratulations! You have just accomplished the trickiest part of each DOS – the design and implementation of filesystem traversal. Depending on the quality of the design, the rest of DOS-specific methods (covered in the remaining sections in this tutorial) will be either a joy or a nightmare to implement (there is rarely anything in between). The latter case should as soon as possible lead to redesigning the filesystem access approach. Such redesign may cover anything from different directory handles up to a different strategy to retrieve these handles. Although it may seem quite straightforward in the just presented case of BS-DOS, the above described approach is a result of two redesign iterations! Nevertheless, as a rule of thumb for designing any filesystem traversal, always directly work with sector data rather than buffer (or pre-process) anything aside in your own memory. The reason is not only an easier and more transparent design but also the fact that all CDos base class helper methods taking as an input parameter a file or directory handle assume that this handle is a direct pointer to some sector data. If sector data retrieval speed is a major concern, still don't buffer anything aside and better consider all options the CImage::GetTrackData method offers (we will see a practical example of this idea later). Last but not least, if you need to represent a file or directory handle in your own memory (we already saw two usages of the TDirectoryEntry constructor), process such handle at the DOS-specific level and make sure such handle isn't passed anywhere into the base classes.

Preparation Works Before the File Manager Tab

Before we engage with the implementation of the File Manager tab, we need to implement several CDos virtual methods, whose current implementation has been "inherited" from the Unknown DOS. The CBSDOS class is still missing the following methods.

class CBSDOS sealed:public CSpectrumDos{
    void GetFileNameOrExt(PCFile file,PTCHAR bufName,PTCHAR bufExt) const override;
    TStdWinError ChangeFileNameAndExt(PFile,LPCTSTR,LPCTSTR,PFile &) override;
    DWORD GetFileSize(PCFile file,PBYTE pnBytesReservedBeforeData,PBYTE pnBytesReservedAfterData,TGetFileSizeOptions option) const override;
    void GetFileTimeStamps(PCFile file,LPFILETIME pCreated,LPFILETIME pLastRead,LPFILETIME pLastWritten) const override;
    void SetFileTimeStamps(PFile file,const FILETIME *pCreated,const FILETIME *pLastRead,const FILETIME *pLastWritten) override;
    DWORD GetAttributes(PCFile file) const override;
    TStdWinError DeleteFile(PFile) override;
    PTCHAR GetFileExportNameAndExt(PCFile,bool,PTCHAR) const override;
    TStdWinError ImportFile(CFile *fIn,DWORD fileSize,LPCTSTR nameAndExtension,DWORD winAttr,PFile &rFile) override;

    // rest of the class omitted for brevity
};

In this section, we will implement all but the GetFileExportNameAndExt and ImportFile which are specific for file/directory export and import, respectively. The other methods have general purpose and not implementing them, we would see nothing in the File Manager tab.

We begin with the simpliest of these methods, GetAttributes. Provided a file or directory handle as the only input parameter, this method extracts attributes from the corresponding directory entry and maps them to Windows standard file attributes, e.g. FILE_ATTRIBUTE_HIDDEN. BS-DOS files and directories don't have any attributes, nevertheless, the directories must be even so indicated with the FILE_ATTRIBUTE_DIRECTORY attribute. Whether a handle describes a directory or file is easily determined by attempting to fit it into the DIRS sector address space – if it fits in, it's a directory, otherwise it's a file handle. We mustn't forget that the root is also a directory.

DWORD CBSDOS::GetAttributes(PCFile file) const{
    // root is a Directory
    if (file==ZX_DIR_ROOT)
        return FILE_ATTRIBUTE_DIRECTORY;
    // root Directory's entries are Subdirectories
    const CDirsSector::PCSlot pSlot=(CDirsSector::PCSlot)file, slots=dirsSector.GetSlots();
    if (slots<=pSlot && pSlot<slots+256)
        return FILE_ATTRIBUTE_DIRECTORY;
    // everything else is a File
    return 0;
}

Moving on, the GetFileSize method determines and returns the count of Bytes a file in question contains. The caller of this method can disambiguate the term "count of Bytes" by choosing one of the below CDos::TGetFileSizeOptions enumeration constants as corresponding input parameter.

Table: CDos::TGetFileSizeOptions enumeration constants to determine the size of a file.
ConstantMeaning
OfficialDataLengthThe CDos::GetFileSize method is requested to return the length (in Bytes) of "official" data, not counting in eventual metadata. For instance, for a file containing only the word Hello, the method should return 5. The caller can further obtain the count of prepended or appended metadata by setting corresponding input pointers to a non-nullptr values. If the file has no metadata, the values referenced by the non-nullptr pointers should be initialized to zero.
SizeOnDiskThe CDos::GetFileSize method is requested to return the number of Bytes the file occupies on the disk, including all metadata, if any. This method should thus virtually always return a whole multiple of the size of one allocation unit (e.g. one sector). For instance, given an allocation unit of 512 Bytes and a file containing only the word Hello, the method should return 512. The implementation of retrieval of length of prepended or appended metadata is optional.

As this method is quite often called and its synopsis verbose, there is a couple of handy non-virtual wrappers around it.

In case of BS-DOS, there are no files known to contain either metadata or "hidden" data (copy-protected files). The implementation of the virtual CDos::GetFileSize method is thus as follows.

DWORD CBSDOS::GetFileSize(PCFile file,PBYTE pnBytesReservedBeforeData,PBYTE pnBytesReservedAfterData,TGetFileSizeOptions option) const{
    if (pnBytesReservedBeforeData) *pnBytesReservedBeforeData=0;
    if (pnBytesReservedAfterData) *pnBytesReservedAfterData=0;
    if (file==DOS_DIR_ROOT)
        // root Directory officially consists only of the DIRS Sector
        return BSDOS_SECTOR_LENGTH_STD;
    else if (IsDirectory(file))
        // root Subdirectory
        return TDirectoryEntry( this, ((CDirsSector::PCSlot)file)->firstSector ).file.dataLength;
    else{
        // File
        const PCDirectoryEntry de=(PCDirectoryEntry)file;
        switch (option){
            case TGetFileSizeOptions::OfficialDataLength:
                return de->file.dataLength;
            case TGetFileSizeOptions::SizeOnDisk:
                return (de->file.dataLength+BSDOS_SECTOR_LENGTH_STD-1)/BSDOS_SECTOR_LENGTH_STD * BSDOS_SECTOR_LENGTH_STD;
            default:
                ASSERT(FALSE); // we shouldn't end up here!
                return 0;
        }
    }
}

As next, we implement the DeleteFile method. Its implementation should fulfil the following general guidelines.

  1. Root directory cannot be deleted.
  2. An already deleted file or directory (supplied as a wrong input to the method, for instance) can be considered as successfull call – the file or directory is deleted, hence the objective of this method is fulfilled.
  3. If there is an error in the sequence of sectors (e.g., a cycle), the file or directory cannot be deleted.
  4. Deleting a subdirectory means deleting also all files it contains.
  5. Once deleted, the corresponding file or subdirectory entry must be marked as empty, ready for re-use, and the containing sector marked as "dirty."

In case of BS-DOS, the guidelines are implemented using the following code. Notice the usage of CDos base class helper methods ShowFileProcessingError (informing about an error that has occured while processing the specified file or directory, MarkDirectorySectorAsDirty (marking a sector containing a specified directory entry as "dirty" – a particular example why you should never buffer any file or directory handle aside in your own memory but instead point to the memory allocated by CImage::GetTrackData method or its wrappers), and finally IsDirectory (which does nothing more than just calls CDos::GetAttributes and searches for the FILE_ATTRIBUTE_DIRECTORY attribute in the result, returning true if it finds it and false otherwise).

TStdWinError CBSDOS::DeleteFile(PFile file){
    if (file==DOS_DIR_ROOT)
        return ERROR_ACCESS_DENIED; // can't delete the root Directory
    CFatPath::PCItem p; DWORD n;
    if (IsDirectory(file)){
        // root Subdirectory
        // a non-existing Subdirectory is successfully deleted
        const CDirsSector::PSlot slot=(CDirsSector::PSlot)file;
        if (!slot->subdirExists)
            return ERROR_SUCCESS;
        // getting the Sectors associated with the Subdirectory
        const CFatPath sectors( this, &TDirectoryEntry(this,slot->firstSector) );
        // if problem with FatPath, the Subdirectory cannot be deleted
        if (const LPCTSTR errMsg=sectors.GetItems(p,n)){
            ShowFileProcessingError(slot,errMsg);
            return ERROR_GEN_FAILURE;
        }
        // deleting all Files
        for( TDirectoryEntry::CTraversal dt(this,slot); dt.GetNextFileOrSubdir(); )
            if (dt.entryType==TDirectoryTraversal::FILE){
                // a File must be successfully deletable
                if (const TStdWinError err=DeleteFile(dt.entry))
                    return err;
            }else
                return ERROR_GEN_FAILURE;
        // marking associated Sectors as Empty
        while (n--)
            __setLogicalSectorFatItem__( p++->value, TFatValue::SectorEmpty );
        // marking Slot as empty
        slot->subdirExists=false;
        dirsSector.MarkAsDirty();
    }else{
        // File
        // a non-existing File is successfully deleted
        const PDirectoryEntry de=(PDirectoryEntry)file;
        if (!de->occupied)
            return ERROR_SUCCESS; // a non-existing File is successfully deleted
        // getting the Sectors associated with the File
        const CFatPath sectors( this, de );
        // if problem with FatPath, the File cannot be deleted
        if (const LPCTSTR errMsg=sectors.GetItems(p,n)){
            ShowFileProcessingError(de,errMsg);
            return ERROR_GEN_FAILURE;
        }
        // marking associated Sectors as Empty
        while (n--)
            __setLogicalSectorFatItem__( p++->value, TFatValue::SectorEmpty );
        // marking DirectoryEntry as empty
        de->occupied=false;
        MarkDirectorySectorAsDirty(de);
    }
    return ERROR_SUCCESS;
}

The remaining four methods to be implemented in this section, GetFileNameOrExt, GetFileTimeStamps, ChangeFileNameAndExt, and SetFileTimeStamps, work with the first directory entry containing information on the directory. Called for a directory slot, a naive approach to GetFileNameOrExt, for instance, might simply instantiate a TDirectoryEntry::CTraversal object and let it retrieve the first directory entry. The object does all its best to get healthy data, whatever the underlying strategy implemented in the container may be – a floppy drive container can, for instance, attempt several times to read an erroneous sector and then re-calibrate the head once or twice. While there is nothing basically wrong with such efforts, the problem is that the File Manager calls the first two methods extensively when displaying directory content, the more if the content must be shown sorted. Imagine sorting BS-DOS root subdirectories by name and one of the names being stored in a bad sector – the head will re-calibrate whenever the name will be requested. If sorting N=128 subdirectories, then the head might re-calibrate up to log N times if we are lucky enough and Quick-sort has been used by the underlying native Windows control (if not, then the head might recalibrate up to full N times). Pursuing an elimination of this overhead, and yet avoiding buffering of names (or whole directory entries) in custom memory, we introduce a helper method that will attempt to get data while forbidding the container to recover from errors. In other words, there will be just one attempt to get these data in scope of GetFileNameOrExt and GetFileTimeStamps methods. If the data haven't been read yet, the container will be given one trial to read them. If the data have been read in the past, the container has them in memory and will return them. Whatever the origin of obtained data, if they are erroneous, we will throw them away. A method that behaves this way is needed only for first directory entry retrieval (i.e. not for files directory entries as this is already solved for at File Manager's side), hence it will be an exclusive part of CDirsSector class.

CBSDOS::PDirectoryEntry CBSDOS::CDirsSector::TryGetDirectoryEntry(PCSlot slot) const{
    if (slot->subdirExists){
        TFdcStatus sr;
        const PBYTE data=bsdos->image->GetSectorData(
            bsdos->__logfyz__(slot->firstSector), 0,
            false, // don't calibrate head, settle with any (already buffered) data, even erroneous
            nullptr, &sr
        );
        if (sr.IsWithoutError())
            return (PDirectoryEntry)data; // although above settled with any, returned are only flawless Data
    }
    return nullptr;
}

Another CDirsSector-based helper method will mark the sector, containing the first directory entry, as "dirty."

void CBSDOS::CDirsSector::MarkDirectoryEntryAsDirty(PCSlot slot) const{
    if (slot->subdirExists)
        bsdos->__markLogicalSectorAsDirty__( slot->firstSector );
}

With this new foundation laid, the GetFileNameOrExt method can already be implemented as follows. Note that we must give also the root directory a name, usually a single backslash for consistency reasons with other platforms. Notice also that the caller can indicate uninterest???in either the name or extension by setting corresponding buffer pointer to nullptr.

#define BSDOS_DIR_CORRUPTED    _T("« Corrupted »")

void CBSDOS::GetFileNameOrExt(PCFile file,PTCHAR bufName,PTCHAR bufExt) const{
    if (file==DOS_DIR_ROOT){
        // root Directory
        if (bufName)
            ::lstrcpy( bufName, _T("\\") );
        if (bufExt)
            *bufExt='\0';
    }else if (IsDirectory(file)){
        // a Subdirectory
        if (bufName)
            if (const PCDirectoryEntry de=dirsSector.TryGetDirectoryEntry( (CDirsSector::PCSlot)file )){
                // Directory's name can be retrieved
                // making sure the dir name matches a file name position in the structure ...
                ASSERT( &de->dir.name==&de->file.stdHeader.name );
                // ... and then leveraging existing routine for file name retrieval
                de->file.stdHeader.GetNameAndExt( bufName, nullptr );
            }else
                // Directory's first Sector is unreadable
                ::lstrcpy( bufName, BSDOS_DIR_CORRUPTED );
        if (bufExt)
            *bufExt='\0';
    }else{
        // File
        const PCDirectoryEntry de=(PCDirectoryEntry)file;
        if (de->fileHasStdHeader){
            // File with a Header
            de->file.stdHeader.GetNameAndExt( bufName, bufExt );
        }else{
            // Headerless File or Fragment
            static DWORD idHeaderless=1;
            if (bufName)
                ::wsprintf( bufName, _T("%08d"), idHeaderless++ ); // ID padded with zeros to eight digits
            if (bufExt)
                *bufExt++=HEADERLESS_EXTENSION, *bufExt='\0';
        }
    }
}

The complementary ChangeFileNameAndExt method has a very similar structure to GetFileNameOrExt. The only significant difference is the checking of whether a file with given name and extension already exists in the directory, by making use of the CDos::FindFileInCurrentDir helper method. This method instantiates a directory traversal object and for each directory entry (reported to contain either a file or subdirectory) calls the above implemented GetFileNameOrExt virtual method to compare the name and extension with. Notice also the last parameter of this method to identify a file or subdirectory handle which should be ignored and identity comparison thus be prevented (i.e., it's not desired to compare a handle with itself).

TStdWinError CBSDOS::ChangeFileNameAndExt(PFile file,LPCTSTR newName,LPCTSTR newExt,PFile &rRenamedFile){
    if (file==ZX_DIR_ROOT)
        // root Directory
        return ERROR_DIR_NOT_ROOT; // can't rename root Directory
    else if (IsDirectory(file)){
        // root Subdirectory
        ASSERT( newName!=nullptr );
        if (const PDirectoryEntry de=dirsSector.TryGetDirectoryEntry( (CDirsSector::PCSlot)file )){
            // Directory's name can be changed
            ASSERT( &de->dir.name==&de->file.stdHeader.name );
            if (const TStdWinError err=de->file.stdHeader.SetNameAndExt( newName, nullptr ))
                return err;
            dirsSector.MarkDirectoryEntryAsDirty( (CDirsSector::PCSlot)file );
            rRenamedFile=file;
            return ERROR_SUCCESS;
        }else
            // Directory's first Sector is unreadable
            return ERROR_SECTOR_NOT_FOUND; // we shouldn't end up here, but just to be sure
    }else{
        // File with Header
        const PDirectoryEntry de=(PDirectoryEntry)file;
        if (de->fileHasStdHeader){
            // File with Header
            ASSERT( newName!=nullptr && newExt!=nullptr );
            // . making sure that a File with given NameAndExtension doesn't yet exist
            if ( rRenamedFile=FindFileInCurrentDir(newName,newExt,file) )
                return ERROR_FILE_EXISTS;
            // . renaming
            if (const TStdWinError err=de->file.stdHeader.SetNameAndExt( newName, newExt ))
                return err;
            MarkDirectorySectorAsDirty(de);
            rRenamedFile=file;
            return ERROR_SUCCESS;
        }else
            // Headerless File or Fragment - ignoring the request
            return ERROR_SUCCESS; // Success to be able to create headerless file copies
    }
}

The implementation of the GetFileTimeStamps method, as its name suggests, retrieves recognized times associated with the file or directory in question – the time of creation, the time of last reading, and the time of last writing. If any of these times doesn't exist or can't be retrieved, the method should set the corresponding output value to TFileDateTime::None. Access to each of these time stamps is individually facilitated by corresponding wrapper method, e.g., CDos::GetFileCreatedTimeStamp returns the time stamp of creation.

void CBSDOS::GetFileTimeStamps(PCFile file,LPFILETIME pCreated,LPFILETIME pLastRead,LPFILETIME pLastWritten) const{
    if (pCreated){
        const PCDirectoryEntry de=IsDirectory(file)
                                  ? dirsSector.TryGetDirectoryEntry( (CDirsSector::PCSlot)file )
                                  : (PCDirectoryEntry)file;
        *pCreated=de!=nullptr
                  ? CMSDOS7::TDateTime(de->dateTimeCreated)
                  : TFileDateTime::None;
    }
    if (pLastRead)
        *pLastRead=TFileDateTime::None;
    if (pLastWritten)
        *pLastWritten=TFileDateTime::None;
}

The implementation of SetFileTimeStamps method sets recognized times associated with the file. Unlike the previous method, this method has no wrappers to selectively change, for instance, just the time stamp of creation, however, the caller can indicate to keep current time stamp unchanged by putting in nullptr as corresponding input parameter.

void CBSDOS::SetFileTimeStamps(PFile file,const FILETIME *pCreated,const FILETIME *pLastRead,const FILETIME *pLastWritten){
    if (pCreated){
        const PDirectoryEntry de=IsDirectory(file)
                                 ? dirsSector.TryGetDirectoryEntry( (CDirsSector::PSlot)file )
                                 : (PDirectoryEntry)file;
        if (de!=nullptr)
            CMSDOS7::TDateTime(*pCreated).ToDWord( &de->dateTimeCreated );
    }
}

Finally after all this work, we are now well prepared to test everything we did so far by displaying the content of any BS-DOS disk.

The File Manager Tab

But before we can enjoy them, we need to implement one more important thing. –… Helpers → Statistics
ahoj
halal

See also