From 0d462bf9a9de816e76b16d65dee8c4b9c45087ab Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 09:50:05 +0200 Subject: [PATCH 1/7] refactor: FCB APIs to use SegmentedAddress types All FCB-related methods in DosInt21Handler and DosFcbManager now accept SegmentedAddress instead of raw linear addresses. This makes segment:offset usage explicit, reduces manual address translation, and improves correctness. Address translation to physical addresses is now handled internally where needed, and FCB structure instantiations are updated accordingly. fix: Inline FCB logs to avoid string allocations Fix XML doc comments: update param descriptions from "Linear address" to "Segmented address" in DosFcbManager Agent-Logs-Url: https://github.com/OpenRakis/Spice86/sessions/49811e96-8ffc-42de-9b9f-50e396e4bdb4 Co-authored-by: maximilien-noal <1087524+maximilien-noal@users.noreply.github.com> tests: fix FCB tests not building refactor: removed unused helper in FCB tests --- .../InterruptHandlers/Dos/DosInt21Handler.cs | 30 +- .../Emulator/OperatingSystem/DosFcbManager.cs | 383 ++++++++++++------ tests/Spice86.Tests/Dos/DosFcbManagerTests.cs | 102 ++--- 3 files changed, 322 insertions(+), 193 deletions(-) diff --git a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs index a15440b980..9faec9fbe6 100644 --- a/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs +++ b/src/Spice86.Core/Emulator/InterruptHandlers/Dos/DosInt21Handler.cs @@ -1963,7 +1963,7 @@ private void FcbOpenFile() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB OPEN FILE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.OpenFile(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.OpenFile(fcbAddress); } /// @@ -1981,7 +1981,7 @@ private void FcbCloseFile() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB CLOSE FILE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.CloseFile(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.CloseFile(fcbAddress); } /// @@ -2001,7 +2001,7 @@ private void FcbFindFirst() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB FIND FIRST at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.FindFirst(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.FindFirst(fcbAddress); } /// @@ -2039,7 +2039,7 @@ private void FcbDeleteFile() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB DELETE FILE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.DeleteFile(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.DeleteFile(fcbAddress); } /// @@ -2061,7 +2061,7 @@ private void FcbRenameFile() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB RENAME FILE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.RenameFile(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.RenameFile(fcbAddress); } /// @@ -2079,7 +2079,7 @@ private void FcbSequentialRead() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB SEQUENTIAL READ at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.SequentialRead(fcbAddress.Linear, GetDtaAddress()); + State.AL = (byte)_dosFcbManager.SequentialRead(fcbAddress, GetDtaAddress()); } /// @@ -2097,7 +2097,7 @@ private void FcbSequentialWrite() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB SEQUENTIAL WRITE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.SequentialWrite(fcbAddress.Linear, GetDtaAddress()); + State.AL = (byte)_dosFcbManager.SequentialWrite(fcbAddress, GetDtaAddress()); } /// @@ -2115,7 +2115,7 @@ private void FcbCreateFile() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB CREATE FILE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.CreateFile(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.CreateFile(fcbAddress); } /// @@ -2133,7 +2133,7 @@ private void FcbRandomRead() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB RANDOM READ at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.RandomRead(fcbAddress.Linear, GetDtaAddress()); + State.AL = (byte)_dosFcbManager.RandomRead(fcbAddress, GetDtaAddress()); } /// @@ -2151,7 +2151,7 @@ private void FcbRandomWrite() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB RANDOM WRITE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.RandomWrite(fcbAddress.Linear, GetDtaAddress()); + State.AL = (byte)_dosFcbManager.RandomWrite(fcbAddress, GetDtaAddress()); } /// @@ -2169,7 +2169,7 @@ private void FcbGetFileSize() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB GET FILE SIZE at {Address}", fcbAddress); } - State.AL = (byte)_dosFcbManager.GetFileSize(fcbAddress.Linear); + State.AL = (byte)_dosFcbManager.GetFileSize(fcbAddress); } /// @@ -2189,7 +2189,7 @@ private void FcbSetRandomRecordNumber() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB SET RANDOM RECORD NUMBER at {Address}", fcbAddress); } - _dosFcbManager.SetRandomRecord(fcbAddress.Linear); + _dosFcbManager.SetRandomRecord(fcbAddress); } /// @@ -2209,7 +2209,7 @@ private void FcbRandomBlockRead() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB RANDOM BLOCK READ at {Address}, {Count} records", fcbAddress, State.CX); } - (FcbStatus status, ushort actualRecordCount) = _dosFcbManager.RandomBlockRead(fcbAddress.Linear, GetDtaAddress(), State.CX); + (FcbStatus status, ushort actualRecordCount) = _dosFcbManager.RandomBlockRead(fcbAddress, GetDtaAddress(), State.CX); State.AL = (byte)status; State.CX = actualRecordCount; } @@ -2231,7 +2231,7 @@ private void FcbRandomBlockWrite() { if (LoggerService.IsEnabled(LogEventLevel.Verbose)) { LoggerService.Verbose("FCB RANDOM BLOCK WRITE at {Address}, {Count} records", fcbAddress, State.CX); } - (FcbStatus status, ushort actualRecordCount) = _dosFcbManager.RandomBlockWrite(fcbAddress.Linear, GetDtaAddress(), State.CX); + (FcbStatus status, ushort actualRecordCount) = _dosFcbManager.RandomBlockWrite(fcbAddress, GetDtaAddress(), State.CX); State.AL = (byte)status; State.CX = actualRecordCount; } @@ -2263,7 +2263,7 @@ private void FcbParseFilename() { stringAddress, fcbAddress, parseControl); } - (FcbParseResult parseStatus, uint bytesAdvanced) = _dosFcbManager.ParseFilename(stringAddress.Linear, fcbAddress.Linear, parseControl); + (FcbParseResult parseStatus, uint bytesAdvanced) = _dosFcbManager.ParseFilename(stringAddress, fcbAddress, parseControl); State.AL = (byte)parseStatus; State.SI += (ushort)bytesAdvanced; } diff --git a/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs b/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs index a38703c841..723055e7be 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs @@ -9,6 +9,7 @@ namespace Spice86.Core.Emulator.OperatingSystem; using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.OperatingSystem.Enums; using Spice86.Core.Emulator.OperatingSystem.Structures; +using Spice86.Shared.Emulator.Memory; using Spice86.Shared.Interfaces; using Spice86.Shared.Utils; @@ -17,8 +18,9 @@ namespace Spice86.Core.Emulator.OperatingSystem; /// /// /// -/// Implementation Reference: This implementation is based on DOSBox Staging dos_files.cpp -/// (https://github.com/dosbox-staging/dosbox-staging) and validated against the MS-DOS Encyclopedia +/// Implementation Reference: This implementation is based on FreeDOS (for the FCB structures),
+/// and DOSBox Staging (for the actual functions, since the two implementations differed) +/// and validated against the MS-DOS Encyclopedia /// (https://www.pcjs.org/documents/books/mspl13/msdos/encyclopedia/appendix-g/). ///
/// @@ -60,12 +62,12 @@ public class DosFcbManager { /// /// INT 21h, AH=29h - Parse filename into an FCB. /// - /// Linear address of the ASCIIZ string to parse. - /// Linear address of the destination FCB (standard or extended). + /// Segmented address of the ASCIIZ string to parse. + /// Segmented address of the destination FCB (standard or extended). /// Parsing control flags. /// A tuple of (FcbParseResult, bytesAdvanced) where bytesAdvanced is the number of bytes consumed from the input string. - public (FcbParseResult, uint) ParseFilename(uint stringAddress, uint fcbAddress, FcbParseControl parseControl) { - string filename = _memory.GetZeroTerminatedString(stringAddress, 128); + public (FcbParseResult, uint) ParseFilename(SegmentedAddress stringAddress, SegmentedAddress fcbAddress, FcbParseControl parseControl) { + string filename = _memory.GetZeroTerminatedString(MemoryUtils.ToPhysicalAddress(stringAddress.Segment, stringAddress.Offset), 128); DosFileControlBlock fcb = GetFcb(fcbAddress, out _); int pos = 0; @@ -255,44 +257,48 @@ private static string ExtractAndPadField(string filename, int startPos, int endP /// /// Gets an FCB wrapper at the given linear address, supporting both standard and extended FCBs. /// - /// Linear address of the FCB (standard or extended). + /// Segmented address of the FCB (standard or extended). /// For extended FCBs, receives the file attributes byte; for standard FCBs, set to 0. /// /// An extended FCB wrapper if the address points to 0xFF (extended FCB flag), /// otherwise a standard FCB wrapper. /// - public DosFileControlBlock GetFcb(uint fcbAddress, out byte attribute) { + public DosFileControlBlock GetFcb(SegmentedAddress fcbAddress, out byte attribute) { attribute = 0; byte flag = _memory.UInt8[fcbAddress]; if (flag == DosExtendedFileControlBlock.ExtendedFcbFlag) { // Extended FCB: header at fcbAddress, standard FCB starts at fcbAddress + 7 - DosExtendedFileControlBlock xfcb = new DosExtendedFileControlBlock(_memory, fcbAddress); + DosExtendedFileControlBlock xfcb = new DosExtendedFileControlBlock(_memory, MemoryUtils.ToPhysicalAddress(fcbAddress.Segment, fcbAddress.Offset)); attribute = xfcb.Attribute; return xfcb; } // Standard FCB - return new DosFileControlBlock(_memory, fcbAddress); + return new DosFileControlBlock(_memory, MemoryUtils.ToPhysicalAddress(fcbAddress.Segment, fcbAddress.Offset)); } /// /// Opens a file using the FCB filename and stores the SFT handle in the FCB. /// - /// Linear address of the FCB. + /// Segmented address of the FCB. /// indicating success or failure. - public FcbStatus OpenFile(uint fcbAddress) { + public FcbStatus OpenFile(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); string fileSpec = fcb.FullFileName; if (string.IsNullOrWhiteSpace(fileSpec)) { - LogFcbWarning("OPEN", baseAddr, "Blank filename"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "OPEN", baseAddr, "Blank filename"); + } return FcbStatus.Error; } DosFileOperationResult result = _dosFileManager.OpenFileOrDevice(fileSpec, FileAccessMode.ReadWrite); if (result.IsError || result.Value == null) { - LogFcbWarning("OPEN", baseAddr, "OpenFileOrDevice failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "OPEN", baseAddr, "OpenFileOrDevice failed"); + } return FcbStatus.Error; } @@ -322,52 +328,64 @@ public FcbStatus OpenFile(uint fcbAddress) { } } - LogFcbDebug("OPEN", baseAddr, fileSpec, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "OPEN", baseAddr, fileSpec, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// Closes an FCB-opened file using its stored SFT handle. + /// Closes an FCB-opened file /// - /// Linear address of the FCB. + /// Segmented address of the FCB. /// indicating success or failure. - public FcbStatus CloseFile(uint fcbAddress) { + public FcbStatus CloseFile(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; if (handle == 0) { - LogFcbWarning("CLOSE", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "CLOSE", baseAddr, "Handle is zero"); + } return FcbStatus.Error; } // Set handle to 0xFF before closing; enables auto-reopen in subsequent read/write fcb.SftNumber = 0xFF; DosFileOperationResult result = _dosFileManager.CloseFileOrDevice(handle); if (result.IsError) { - LogFcbWarning("CLOSE", baseAddr, "CloseFileOrDevice failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "CLOSE", baseAddr, "CloseFileOrDevice failed"); + } return FcbStatus.Error; } - LogFcbDebug("CLOSE", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "CLOSE", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return FcbStatus.Success; } /// /// Creates a file using the FCB filename and stores the SFT handle in the FCB. /// - /// Linear address of the FCB. + /// Segmented address of the FCB. /// indicating success or failure. - public FcbStatus CreateFile(uint fcbAddress) { + public FcbStatus CreateFile(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); string fileSpec = fcb.FullFileName; if (string.IsNullOrWhiteSpace(fileSpec)) { - LogFcbWarning("CREATE", baseAddr, "Blank filename"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "CREATE", baseAddr, "Blank filename"); + } return FcbStatus.Error; } DosFileOperationResult result = _dosFileManager.CreateFileUsingHandle(fileSpec, 0); if (result.IsError || result.Value == null) { - LogFcbWarning("CREATE", baseAddr, "CreateFileUsingHandle failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "CREATE", baseAddr, "CreateFileUsingHandle failed"); + } return FcbStatus.Error; } @@ -375,17 +393,19 @@ public FcbStatus CreateFile(uint fcbAddress) { fcb.SftNumber = (byte)handle; // Always set RecordSize to 128 on create fcb.RecordSize = DosFileControlBlock.DefaultRecordSize; - LogFcbDebug("CREATE", baseAddr, fileSpec, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "CREATE", baseAddr, fileSpec, FcbStatus.Success); + } return FcbStatus.Success; } /// /// INT 21h AH=14h sequential read using the current block/record pointer. /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// describing the outcome. - public FcbStatus SequentialRead(uint fcbAddress, uint dtaAddress) { + public FcbStatus SequentialRead(SegmentedAddress fcbAddress, uint dtaAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -394,16 +414,22 @@ public FcbStatus SequentialRead(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("SEQ READ", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ READ", baseAddr, "Auto-reopen failed"); + } return FcbStatus.NoData; } fcb = new DosFileControlBlock(_memory, baseAddr); handle = fcb.SftNumber; - LogFcbDebug("SEQ READ", baseAddr, "Reopened closed FCB", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "SEQ READ", baseAddr, "Reopened closed FCB", FcbStatus.Success); + } } if (handle == 0) { - LogFcbWarning("SEQ READ", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ READ", baseAddr, "Handle is zero"); + } return FcbStatus.Error; } @@ -413,18 +439,24 @@ public FcbStatus SequentialRead(uint fcbAddress, uint dtaAddress) { int recordSize = fcb.RecordSize; uint absoluteRecord = fcb.AbsoluteRecord; if (!TryComputeOffset(absoluteRecord, recordSize, out int offset, out FcbStatus offsetStatus)) { - LogFcbWarning("SEQ READ", baseAddr, "Offset overflow"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ READ", baseAddr, "Offset overflow"); + } return offsetStatus; } DosFileOperationResult seek = _dosFileManager.MoveFilePointerUsingHandle(SeekOrigin.Begin, handle, offset); if (seek.IsError) { - LogFcbWarning("SEQ READ", baseAddr, "Seek failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ READ", baseAddr, "Seek failed"); + } return FcbStatus.Error; } DosFileOperationResult read = _dosFileManager.ReadFileOrDevice(handle, (ushort)recordSize, dtaAddress); if (read.IsError) { - LogFcbWarning("SEQ READ", baseAddr, "Read failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ READ", baseAddr, "Read failed"); + } return FcbStatus.Error; } @@ -442,17 +474,19 @@ public FcbStatus SequentialRead(uint fcbAddress, uint dtaAddress) { if (len < recordSize) { return FcbStatus.EndOfFile; } - LogFcbDebug("SEQ READ", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "SEQ READ", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// INT 21h AH=15h sequential write using the current block/record pointer. + /// INT 21h AH=15h sequential write /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// describing the outcome. - public FcbStatus SequentialWrite(uint fcbAddress, uint dtaAddress) { + public FcbStatus SequentialWrite(SegmentedAddress fcbAddress, uint dtaAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -461,16 +495,22 @@ public FcbStatus SequentialWrite(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("SEQ WRITE", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ WRITE", baseAddr, "Auto-reopen failed"); + } return FcbStatus.NoData; } fcb = new DosFileControlBlock(_memory, baseAddr); handle = fcb.SftNumber; - LogFcbDebug("SEQ WRITE", baseAddr, "Reopened closed FCB", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "SEQ WRITE", baseAddr, "Reopened closed FCB", FcbStatus.Success); + } } if (handle == 0) { - LogFcbWarning("SEQ WRITE", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ WRITE", baseAddr, "Handle is zero"); + } return FcbStatus.Error; } @@ -480,18 +520,24 @@ public FcbStatus SequentialWrite(uint fcbAddress, uint dtaAddress) { int recordSize = fcb.RecordSize; uint absoluteRecord = fcb.AbsoluteRecord; if (!TryComputeOffset(absoluteRecord, recordSize, out int offset, out FcbStatus offsetStatus)) { - LogFcbWarning("SEQ WRITE", baseAddr, "Offset overflow"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ WRITE", baseAddr, "Offset overflow"); + } return offsetStatus; } DosFileOperationResult seek = _dosFileManager.MoveFilePointerUsingHandle(SeekOrigin.Begin, handle, offset); if (seek.IsError) { - LogFcbWarning("SEQ WRITE", baseAddr, "Seek failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ WRITE", baseAddr, "Seek failed"); + } return FcbStatus.Error; } DosFileOperationResult write = _dosFileManager.WriteToFileOrDevice(handle, (ushort)recordSize, dtaAddress); if (write.IsError) { - LogFcbWarning("SEQ WRITE", baseAddr, "Write failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "SEQ WRITE", baseAddr, "Write failed"); + } return FcbStatus.Error; } ushort len = (ushort)(write.Value ?? 0); @@ -503,17 +549,19 @@ public FcbStatus SequentialWrite(uint fcbAddress, uint dtaAddress) { if (len < recordSize) { return FcbStatus.NoData; } - LogFcbDebug("SEQ WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "SEQ WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// INT 21h AH=21h random read using the RandomRecord field. + /// INT 21h AH=21h random read /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// describing the outcome. - public FcbStatus RandomRead(uint fcbAddress, uint dtaAddress) { + public FcbStatus RandomRead(SegmentedAddress fcbAddress, uint dtaAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -522,7 +570,9 @@ public FcbStatus RandomRead(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("RAND READ", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND READ", baseAddr, "Auto-reopen failed"); + } return FcbStatus.NoData; } fcb = new DosFileControlBlock(_memory, baseAddr); @@ -530,7 +580,9 @@ public FcbStatus RandomRead(uint fcbAddress, uint dtaAddress) { } if (handle == 0) { - LogFcbWarning("RAND READ", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND READ", baseAddr, "Handle is zero"); + } return FcbStatus.Error; } @@ -550,18 +602,24 @@ public FcbStatus RandomRead(uint fcbAddress, uint dtaAddress) { uint absoluteRecord = fcb.AbsoluteRecord; if (!TryComputeOffset(absoluteRecord, recordSize, out int offset, out FcbStatus offsetStatus)) { - LogFcbWarning("RAND READ", baseAddr, "Offset overflow"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND READ", baseAddr, "Offset overflow"); + } return offsetStatus; } DosFileOperationResult seek = _dosFileManager.MoveFilePointerUsingHandle(SeekOrigin.Begin, handle, offset); if (seek.IsError) { - LogFcbWarning("RAND READ", baseAddr, "Seek failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND READ", baseAddr, "Seek failed"); + } return FcbStatus.Error; } DosFileOperationResult read = _dosFileManager.ReadFileOrDevice(handle, (ushort)recordSize, dtaAddress); if (read.IsError) { - LogFcbWarning("RAND READ", baseAddr, "Read failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND READ", baseAddr, "Read failed"); + } return FcbStatus.Error; } ushort len = (ushort)(read.Value ?? 0); @@ -581,17 +639,19 @@ public FcbStatus RandomRead(uint fcbAddress, uint dtaAddress) { if (len < recordSize) { return FcbStatus.EndOfFile; } - LogFcbDebug("RAND READ", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RAND READ", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// INT 21h AH=22h random write using the RandomRecord field. + /// INT 21h AH=22h random write /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// describing the outcome. - public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { + public FcbStatus RandomWrite(SegmentedAddress fcbAddress, uint dtaAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -600,7 +660,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("RAND WRITE", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND WRITE", baseAddr, "Auto-reopen failed"); + } return FcbStatus.NoData; } fcb = new DosFileControlBlock(_memory, baseAddr); @@ -608,7 +670,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { } if (handle == 0) { - LogFcbWarning("RAND WRITE", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND WRITE", baseAddr, "Handle is zero"); + } return FcbStatus.Error; } @@ -626,18 +690,24 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { uint absoluteRecord = fcb.AbsoluteRecord; if (!TryComputeOffset(absoluteRecord, recordSize, out int offset, out FcbStatus offsetStatus)) { - LogFcbWarning("RAND WRITE", baseAddr, "Offset overflow"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND WRITE", baseAddr, "Offset overflow"); + } return offsetStatus; } DosFileOperationResult seek = _dosFileManager.MoveFilePointerUsingHandle(SeekOrigin.Begin, handle, offset); if (seek.IsError) { - LogFcbWarning("RAND WRITE", baseAddr, "Seek failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND WRITE", baseAddr, "Seek failed"); + } return FcbStatus.Error; } DosFileOperationResult write = _dosFileManager.WriteToFileOrDevice(handle, (ushort)recordSize, dtaAddress); if (write.IsError) { - LogFcbWarning("RAND WRITE", baseAddr, "Write failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND WRITE", baseAddr, "Write failed"); + } return FcbStatus.Error; } ushort len = (ushort)(write.Value ?? 0); @@ -652,18 +722,20 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { if (len < recordSize) { return FcbStatus.NoData; } - LogFcbDebug("RAND WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RAND WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// INT 21h AH=27h random block read from the RandomRecord pointer. + /// INT 21h AH=27h random block read /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// Number of records requested to read. /// A tuple of (FcbStatus, actualRecordCount) describing the outcome and number of records actually read. - public (FcbStatus, ushort) RandomBlockRead(uint fcbAddress, uint dtaAddress, ushort requestedRecordCount) { + public (FcbStatus, ushort) RandomBlockRead(SegmentedAddress fcbAddress, uint dtaAddress, ushort requestedRecordCount) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -672,7 +744,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("RAND BLK READ", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND BLK READ", baseAddr, "Auto-reopen failed"); + } return (FcbStatus.NoData, 0); } fcb = new DosFileControlBlock(_memory, baseAddr); @@ -680,7 +754,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { } if (handle == 0) { - LogFcbWarning("RAND BLK READ", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND BLK READ", baseAddr, "Handle is zero"); + } return (FcbStatus.Error, 0); } if (fcb.RecordSize == 0) { @@ -751,11 +827,11 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { /// /// INT 21h AH=28h random block write from the RandomRecord pointer. /// - /// Linear address of the FCB. - /// Linear address of the DTA buffer. + /// Segmented address of the FCB. + /// Segmented address of the DTA buffer. /// Number of records requested to write (0 = truncate file to random record position). /// A tuple of (FcbStatus, actualRecordCount) describing the outcome and number of records actually written. - public (FcbStatus, ushort) RandomBlockWrite(uint fcbAddress, uint dtaAddress, ushort requestedRecordCount) { + public (FcbStatus, ushort) RandomBlockWrite(SegmentedAddress fcbAddress, uint dtaAddress, ushort requestedRecordCount) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); ushort handle = fcb.SftNumber; @@ -764,7 +840,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { if (handle == 0xFF && fcb.RecordSize != 0) { FcbStatus reopenStatus = OpenFile(fcbAddress); if (reopenStatus != FcbStatus.Success) { - LogFcbWarning("RAND BLK WRITE", baseAddr, "Auto-reopen failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND BLK WRITE", baseAddr, "Auto-reopen failed"); + } return (FcbStatus.NoData, 0); } fcb = new DosFileControlBlock(_memory, baseAddr); @@ -772,7 +850,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { } if (handle == 0) { - LogFcbWarning("RAND BLK WRITE", baseAddr, "Handle is zero"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND BLK WRITE", baseAddr, "Handle is zero"); + } return (FcbStatus.Error, 0); } if (fcb.RecordSize == 0) { @@ -795,10 +875,14 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { // Update size/date/time fcb.FileSize = (uint)offset; UpdateFcbDateTime(fcb); - LogFcbDebug("RAND BLK WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RAND BLK WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return (FcbStatus.Success, 0); } - LogFcbWarning("RAND BLK WRITE", baseAddr, "Handle is not a file"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RAND BLK WRITE", baseAddr, "Handle is not a file"); + } return (FcbStatus.Error, 0); } @@ -852,7 +936,9 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { if (lastError != FcbStatus.Success) { return (lastError, totalWritten); } - LogFcbDebug("RAND BLK WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RAND BLK WRITE", baseAddr, fcb.FullFileName, FcbStatus.Success); + } return (FcbStatus.Success, totalWritten); } @@ -861,19 +947,23 @@ public FcbStatus RandomWrite(uint fcbAddress, uint dtaAddress) { /// Opens the file, divides size by record size using ceiling division, stores result in FCB.RandomRecord, /// then closes the file. If RecordSize is 0, uses default 128 bytes. ///
- /// Linear address of the FCB containing the filename to query. + /// Segmented address of the FCB containing the filename to query. /// if file size retrieved, if file not found or device. - public FcbStatus GetFileSize(uint fcbAddress) { + public FcbStatus GetFileSize(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); string fileSpec = fcb.FullFileName; if (string.IsNullOrWhiteSpace(fileSpec)) { - LogFcbWarning("GET SIZE", baseAddr, "Blank filename"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "GET SIZE", baseAddr, "Blank filename"); + } return FcbStatus.Error; } DosFileOperationResult open = _dosFileManager.OpenFileOrDevice(fileSpec, FileAccessMode.ReadOnly); if (open.IsError || open.Value == null) { - LogFcbWarning("GET SIZE", baseAddr, "OpenFileOrDevice failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "GET SIZE", baseAddr, "OpenFileOrDevice failed"); + } return FcbStatus.Error; } ushort handle = (ushort)open.Value.Value; @@ -887,25 +977,31 @@ public FcbStatus GetFileSize(uint fcbAddress) { } fcb.RandomRecord = records; _dosFileManager.CloseFileOrDevice(handle); - LogFcbDebug("GET SIZE", baseAddr, fileSpec, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "GET SIZE", baseAddr, fileSpec, FcbStatus.Success); + } return FcbStatus.Success; } _dosFileManager.CloseFileOrDevice(handle); - LogFcbWarning("GET SIZE", baseAddr, "Not a DOS file"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "GET SIZE", baseAddr, "Not a DOS file"); + } return FcbStatus.Error; } /// /// INT 21h AH=13h delete files matching the FCB pattern. /// - /// Linear address of the FCB. + /// Segmented address of the FCB. /// describing the outcome. - public FcbStatus DeleteFile(uint fcbAddress) { + public FcbStatus DeleteFile(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); string pattern = fcb.FullFileName; if (string.IsNullOrWhiteSpace(pattern)) { - LogFcbWarning("DELETE", baseAddr, "Blank pattern"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "DELETE", baseAddr, "Blank pattern"); + } return FcbStatus.Error; } DosFileOperationResult ff = _dosFileManager.FindFirstMatchingFile(pattern, 0); @@ -917,7 +1013,9 @@ public FcbStatus DeleteFile(uint fcbAddress) { string dosName = dta.FileName; DosFileOperationResult del = _dosFileManager.RemoveFile(dosName); if (del.IsError) { - LogFcbWarning("DELETE", baseAddr, "RemoveFile failed"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "DELETE", baseAddr, "RemoveFile failed"); + } return FcbStatus.Error; } DosFileOperationResult nx = _dosFileManager.FindNextMatchingFile(); @@ -925,16 +1023,18 @@ public FcbStatus DeleteFile(uint fcbAddress) { break; } } - LogFcbDebug("DELETE", baseAddr, pattern, FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "DELETE", baseAddr, pattern, FcbStatus.Success); + } return FcbStatus.Success; } /// - /// INT 21h AH=17h rename files using FCB old/new patterns (supports wildcards). + /// INT 21h AH=17h rename /// - /// Linear address of the FCB. + /// Segmented address of the FCB. /// describing the outcome. - public FcbStatus RenameFile(uint fcbAddress) { + public FcbStatus RenameFile(SegmentedAddress fcbAddress) { // Special FCB layout for rename: old name/ext then new name/ext in reserved region uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); @@ -942,16 +1042,22 @@ public FcbStatus RenameFile(uint fcbAddress) { string oldName = GetRenameOldName(fcb); string newPattern = GetRenameNewName(baseAddr); - LogFcbDebug("RENAME START", baseAddr, $"Pattern: {oldName} -> {newPattern}", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RENAME START", baseAddr, $"Pattern: {oldName} -> {newPattern}", FcbStatus.Success); + } if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newPattern)) { - LogFcbWarning("RENAME", baseAddr, "Missing old/new names"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, "Missing old/new names"); + } return FcbStatus.Error; } DosFileOperationResult ff = _dosFileManager.FindFirstMatchingFile(oldName, 0); if (ff.IsError) { - LogFcbWarning("RENAME", baseAddr, $"FindFirst failed for pattern: {oldName}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"FindFirst failed for pattern: {oldName}"); + } return FcbStatus.Error; } @@ -964,17 +1070,23 @@ public FcbStatus RenameFile(uint fcbAddress) { string sourceDos = dta.FileName; string destDos = ApplyWildcardRename(sourceDos, newPattern); - LogFcbDebug("RENAME FILE", baseAddr, $"{sourceDos} -> {destDos}", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RENAME FILE", baseAddr, $"{sourceDos} -> {destDos}", FcbStatus.Success); + } string? srcHost = _dosFileManager.TryGetFullHostPathFromDos(sourceDos); if (string.IsNullOrWhiteSpace(srcHost)) { - LogFcbWarning("RENAME", baseAddr, $"Unable to resolve source path: {sourceDos}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Unable to resolve source path: {sourceDos}"); + } return FcbStatus.Error; } // Skip duplicates (same file with different case) if (seenSources.Contains(srcHost)) { - LogFcbDebug("RENAME SKIP", baseAddr, $"Already collected: {srcHost}", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RENAME SKIP", baseAddr, $"Already collected: {srcHost}", FcbStatus.Success); + } DosFileOperationResult next = _dosFileManager.FindNextMatchingFile(); if (next.IsError) { break; @@ -986,24 +1098,32 @@ public FcbStatus RenameFile(uint fcbAddress) { // For destination, construct path in same directory as source string? srcDir = Path.GetDirectoryName(srcHost); if (string.IsNullOrWhiteSpace(srcDir)) { - LogFcbWarning("RENAME", baseAddr, $"Unable to get source directory: {srcHost}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Unable to get source directory: {srcHost}"); + } return FcbStatus.Error; } string dstHost = Path.Join(srcDir, destDos); if (!File.Exists(srcHost)) { - LogFcbWarning("RENAME", baseAddr, $"Source does not exist: {srcHost}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Source does not exist: {srcHost}"); + } return FcbStatus.Error; } if (File.Exists(dstHost)) { - LogFcbWarning("RENAME", baseAddr, $"Destination exists: {dstHost}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Destination exists: {dstHost}"); + } return FcbStatus.Error; } // Skip if this destination already appears in the rename list (wildcard conflict) if (seenDestinations.Contains(dstHost)) { - LogFcbWarning("RENAME", baseAddr, $"Destination conflict in pattern: {dstHost}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Destination conflict in pattern: {dstHost}"); + } return FcbStatus.Error; } seenDestinations.Add(dstHost); @@ -1019,19 +1139,27 @@ public FcbStatus RenameFile(uint fcbAddress) { // Now rename all collected files int renameCount = 0; foreach ((string srcHost, string dstHost) in filesToRename) { - LogFcbDebug("RENAME EXECUTE", baseAddr, $"Moving {srcHost} to {dstHost}", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RENAME EXECUTE", baseAddr, $"Moving {srcHost} to {dstHost}", FcbStatus.Success); + } try { File.Move(srcHost, dstHost); renameCount++; } catch (IOException ex) { - LogFcbWarning("RENAME", baseAddr, $"Failed to rename {srcHost}: {ex.Message}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Failed to rename {srcHost}: {ex.Message}"); + } return FcbStatus.Error; } catch (UnauthorizedAccessException ex) { - LogFcbWarning("RENAME", baseAddr, $"Access denied renaming {srcHost}: {ex.Message}"); + if (_loggerService.IsEnabled(LogEventLevel.Warning)) { + _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", "RENAME", baseAddr, $"Access denied renaming {srcHost}: {ex.Message}"); + } return FcbStatus.Error; } } - LogFcbDebug("RENAME", baseAddr, $"{oldName} -> {newPattern} ({renameCount} files)", FcbStatus.Success); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "RENAME", baseAddr, $"{oldName} -> {newPattern} ({renameCount} files)", FcbStatus.Success); + } return FcbStatus.Success; } @@ -1039,14 +1167,16 @@ public FcbStatus RenameFile(uint fcbAddress) { /// INT 21h AH=11h FCB Find First using the FCB filename pattern. /// Populates the DTA in FCB format (drive + 8-byte name + 3-byte ext + metadata). ///
- /// Linear address of the FCB. + /// Segmented address of the FCB. /// describing the outcome. - public FcbStatus FindFirst(uint fcbAddress) { + public FcbStatus FindFirst(SegmentedAddress fcbAddress) { DosFileControlBlock fcb = GetFcb(fcbAddress, out byte attribute); string pattern = fcb.FullFileName; DosFileOperationResult result = _dosFileManager.FcbFindFirstMatchingFile(pattern, attribute); FcbStatus status = result.IsError ? FcbStatus.Error : FcbStatus.Success; - LogFcbDebug("FIND FIRST", fcb.BaseAddress, pattern, status); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "FIND FIRST", fcb.BaseAddress, pattern, status); + } return status; } @@ -1057,27 +1187,29 @@ public FcbStatus FindFirst(uint fcbAddress) { public FcbStatus FindNext() { DosFileOperationResult result = _dosFileManager.FindNextMatchingFile(); FcbStatus status = result.IsError ? FcbStatus.Error : FcbStatus.Success; - LogFcbDebug("FIND NEXT", 0, string.Empty, status); + if (_loggerService.IsEnabled(LogEventLevel.Debug)) { + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "FIND NEXT", 0, string.Empty, status); + } return status; } /// /// INT 21h AH=24h - Sets the random record field from the current block and record fields. /// - /// Linear address of the FCB. - public void SetRandomRecord(uint fcbAddress) { + /// Segmented address of the FCB. + public void SetRandomRecord(SegmentedAddress fcbAddress) { uint baseAddr = GetActualFcbBaseAddress(fcbAddress); DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); fcb.SetRandomFromPosition(); } - private uint GetActualFcbBaseAddress(uint fcbAddress) { + private uint GetActualFcbBaseAddress(SegmentedAddress fcbAddress) { // Extended FCB detection via drive marker 0xFF - DosFileControlBlock probe = new DosFileControlBlock(_memory, fcbAddress); + DosFileControlBlock probe = new DosFileControlBlock(_memory, MemoryUtils.ToPhysicalAddress(fcbAddress.Segment, fcbAddress.Offset)); if (probe.DriveNumber == 0xFF) { - return fcbAddress + DosExtendedFileControlBlock.HeaderSize; + return MemoryUtils.ToPhysicalAddress(fcbAddress.Segment, fcbAddress.Offset) + DosExtendedFileControlBlock.HeaderSize; } - return fcbAddress; + return MemoryUtils.ToPhysicalAddress(fcbAddress.Segment, fcbAddress.Offset); } private static string ConvertFcbPatternToDosPattern(string fcbName, string fcbExt) { @@ -1206,7 +1338,7 @@ private string ReadSpacePaddedField(uint fcbBaseAddress, int offset, int length) /// /// Zero-pads the DTA buffer from the end of read data to the full record size. /// - /// Linear address of the DTA buffer. + /// Segmented address of the DTA buffer. /// Number of bytes actually read. /// Full record size to pad to. private void ZeroPadDta(uint dtaAddress, int bytesRead, int recordSize) { @@ -1239,15 +1371,4 @@ private void UpdateFcbDateTime(DosFileControlBlock fcb) { fcb.Time = DosFileManager.ToDosTime(now); } - private void LogFcbWarning(string operation, uint fcbBaseAddress, string reason) { - if (_loggerService.IsEnabled(LogEventLevel.Warning)) { - _loggerService.Warning("FCB {Operation} failed at 0x{Address:X} because {Reason}", operation, fcbBaseAddress, reason); - } - } - - private void LogFcbDebug(string operation, uint fcbBaseAddress, string detail, FcbStatus status) { - if (_loggerService.IsEnabled(LogEventLevel.Debug)) { - _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", operation, fcbBaseAddress, detail, status); - } - } } \ No newline at end of file diff --git a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs index 5793ec0529..726ce139ff 100644 --- a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs +++ b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs @@ -5,6 +5,7 @@ using Spice86.Core.Emulator.Memory; using Spice86.Core.Emulator.OperatingSystem.Enums; using Spice86.Core.Emulator.OperatingSystem.Structures; +using Spice86.Shared.Emulator.Memory; using Spice86.Tests.Utility; using Xunit; @@ -14,6 +15,9 @@ public class DosFcbManagerTests : IDisposable { private const uint FcbAddr = 0x2000; private const uint DtaAddr = 0x3000; + private static readonly SegmentedAddress StringPointer = ToSegmentedAddress(StringAddr); + private static readonly SegmentedAddress FcbPointer = ToSegmentedAddress(FcbAddr); + private readonly TempFile _tempFile; private readonly DosTestFixture _fixture; @@ -27,6 +31,10 @@ public void Dispose() { _tempFile.Dispose(); } + private static SegmentedAddress ToSegmentedAddress(uint physicalAddress) { + return new SegmentedAddress((ushort)(physicalAddress >> 4), (ushort)(physicalAddress & 0xF)); + } + private static void WriteSpacePaddedField(IMemory memory, uint address, string value, int fieldSize) { byte[] buffer = new byte[fieldSize]; for (int i = 0; i < fieldSize; i++) { @@ -74,7 +82,7 @@ public void ParseFilename_SimpleFilename_NoWildcards() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "TEST.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -91,7 +99,7 @@ public void ParseFilename_WithDrive_ValidDrive() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "C:FILE.DAT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, 0); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, 0); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -108,7 +116,7 @@ public void ParseFilename_InvalidDrive_ContinuesParsing() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "Q:TEST.TXT", 128); // Act - Undocumented behavior: should keep parsing even if drive specification is invalid - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, 0); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, 0); // Assert result.Should().Be(FcbParseResult.InvalidDrive); @@ -125,7 +133,7 @@ public void ParseFilename_Wildcards_Asterisk() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "*.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.WildcardsPresent); @@ -141,7 +149,7 @@ public void ParseFilename_Wildcards_QuestionMark() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "TEST?.TX?", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.WildcardsPresent); @@ -157,7 +165,7 @@ public void ParseFilename_SkipLeadingSeparators() { _fixture.Memory.SetZeroTerminatedString(StringAddr, " :;,=+ TEST.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.SkipLeadingSeparators | FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.SkipLeadingSeparators | FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -172,7 +180,7 @@ public void ParseFilename_WhitespaceAlwaysSkipped() { _fixture.Memory.SetZeroTerminatedString(StringAddr, " \t TEST.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, 0); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, 0); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -187,7 +195,7 @@ public void ParseFilename_Dot_ParsesCorrectly() { _fixture.Memory.SetZeroTerminatedString(StringAddr, ".", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -202,7 +210,7 @@ public void ParseFilename_DotDot_ParsesCorrectly() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "..", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -217,7 +225,7 @@ public void ParseFilename_NoExtension() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "NOEXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -233,7 +241,7 @@ public void ParseFilename_UppercaseConversion() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "lowercase.ext", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -249,7 +257,7 @@ public void ParseFilename_ParseControlFlags() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "TEST.TXT", 128); // Act - PARSE_BLNK_FNAME: should blank filename field - (FcbParseResult result, _) = _fixture.DosFcbManager.ParseFilename(StringAddr, FcbAddr, FcbParseControl.BlankFilename); + (FcbParseResult result, _) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.BlankFilename); // Assert - filename field should be blanked before parsing result.Should().Be(FcbParseResult.NoWildcards); @@ -263,7 +271,7 @@ public void CreateFile_ValidName_ReturnsSuccess() { DosFileControlBlock fcb = CreateFcb("NEWFILE", "TXT"); // Act - FcbStatus status = _fixture.DosFcbManager.CreateFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.CreateFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -271,7 +279,7 @@ public void CreateFile_ValidName_ReturnsSuccess() { fcb.RecordSize.Should().Be(DosFileControlBlock.DefaultRecordSize); // Cleanup - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); } [Fact] @@ -281,7 +289,7 @@ public void OpenFile_ExistingFile_ReturnsSuccess() { DosFileControlBlock fcb = CreateFcb("TESTOPEN", "TXT"); // Act - FcbStatus status = _fixture.DosFcbManager.OpenFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.OpenFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -289,7 +297,7 @@ public void OpenFile_ExistingFile_ReturnsSuccess() { fcb.RecordSize.Should().Be(DosFileControlBlock.DefaultRecordSize); // Cleanup - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); } [Fact] @@ -303,7 +311,7 @@ public void OpenFile_ExistingFile_PopulatesFileMetadata() { fcb.Time = 0; // Act - FcbStatus status = _fixture.DosFcbManager.OpenFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.OpenFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -318,17 +326,17 @@ public void OpenFile_ExistingFile_PopulatesFileMetadata() { hour.Should().BeLessThanOrEqualTo((ushort)23); // Cleanup - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); } [Fact] public void CloseFile_OpenedFile_ReturnsSuccess() { // Arrange CreateFcb("TESTCLS", "TXT"); - _fixture.DosFcbManager.CreateFile(FcbAddr); + _fixture.DosFcbManager.CreateFile(FcbPointer); // Act - FcbStatus status = _fixture.DosFcbManager.CloseFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.CloseFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -338,7 +346,7 @@ public void CloseFile_OpenedFile_ReturnsSuccess() { public void SequentialWriteAndRead_RoundTrip_DataPreserved() { // Arrange CreateFcb("RWTST", "DAT"); - _fixture.DosFcbManager.CreateFile(FcbAddr); + _fixture.DosFcbManager.CreateFile(FcbPointer); byte[] testData = new byte[128]; for (int i = 0; i < 128; i++) { testData[i] = (byte)('A' + (i % 26)); @@ -346,13 +354,13 @@ public void SequentialWriteAndRead_RoundTrip_DataPreserved() { } // Act - FcbStatus writeStatus = _fixture.DosFcbManager.SequentialWrite(FcbAddr, DtaAddr); - _fixture.DosFcbManager.CloseFile(FcbAddr); - _fixture.DosFcbManager.OpenFile(FcbAddr); + FcbStatus writeStatus = _fixture.DosFcbManager.SequentialWrite(FcbPointer, DtaAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); + _fixture.DosFcbManager.OpenFile(FcbPointer); for (int i = 0; i < 128; i++) { _fixture.Memory.UInt8[DtaAddr + (uint)i] = 0; } - FcbStatus readStatus = _fixture.DosFcbManager.SequentialRead(FcbAddr, DtaAddr); + FcbStatus readStatus = _fixture.DosFcbManager.SequentialRead(FcbPointer, DtaAddr); // Assert writeStatus.Should().Be(FcbStatus.Success); @@ -362,7 +370,7 @@ public void SequentialWriteAndRead_RoundTrip_DataPreserved() { } // Cleanup - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); } [Fact] @@ -392,7 +400,7 @@ public void RenameFile_SimpleWildcardExtension_RenamesAllMatches() { WriteSpacePaddedField(_fixture.Memory, FcbAddr + 25, "OUT", 3); // Act - FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -430,7 +438,7 @@ public void RenameFile_PrefixWildcard_RenamesMatchingPrefix() { WriteSpacePaddedField(_fixture.Memory, FcbAddr + 25, "OUT", 3); // Act - FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -467,7 +475,7 @@ public void RenameFile_ComplexWildcardPattern_HandlesCorrectly() { WriteSpacePaddedField(_fixture.Memory, FcbAddr + 25, "???", 3); // Act - FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -505,7 +513,7 @@ public void RenameFile_ShortenExtension_TruncatesCorrectly() { WriteSpacePaddedField(_fixture.Memory, FcbAddr + 25, "?? ", 3); // Act - FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbAddr); + FcbStatus status = _fixture.DosFcbManager.RenameFile(FcbPointer); // Assert status.Should().Be(FcbStatus.Success); @@ -530,7 +538,7 @@ public void SetRandomRecord_CalculatesCorrectly() { fcb.RecordSize = 128; // Act - _fixture.DosFcbManager.SetRandomRecord(FcbAddr); + _fixture.DosFcbManager.SetRandomRecord(FcbPointer); // Assert - random record = (currentBlock * 128) + currentRecord const uint expectedRandom = (5u * 128) + 42; @@ -543,7 +551,7 @@ public void GetFcb_StandardFcb_ReturnsCorrectly() { CreateFcb("TESTFILE", "TXT"); // Act - DosFileControlBlock result = _fixture.DosFcbManager.GetFcb(FcbAddr, out byte attr); + DosFileControlBlock result = _fixture.DosFcbManager.GetFcb(FcbPointer, out byte attr); // Assert result.BaseAddress.Should().Be(FcbAddr); @@ -562,7 +570,7 @@ public void GetFcb_ExtendedFcb_ReturnsAttributeAndEmbeddedFcb() { xfcb.FileExtension = "DAT"; // Act - DosFileControlBlock result = _fixture.DosFcbManager.GetFcb(FcbAddr, out byte attr); + DosFileControlBlock result = _fixture.DosFcbManager.GetFcb(FcbPointer, out byte attr); // Assert attr.Should().Be(0x20); @@ -577,13 +585,13 @@ public void RandomBlockRead_MultipleRecords_AdvancesDtaForEachRecord() { byte[] fileData = { 0x41, 0x41, 0x41, 0x41, 0x42, 0x42, 0x42, 0x42, 0x43, 0x43, 0x43, 0x43 }; CreateTestFile("MULTIREC.DAT", fileData); DosFileControlBlock fcb = CreateFcb("MULTIREC", "DAT"); - _fixture.DosFcbManager.OpenFile(FcbAddr); + _fixture.DosFcbManager.OpenFile(FcbPointer); fcb.RecordSize = 4; fcb.RandomRecord = 0; ushort requestedRecordCount = 3; // Act - (FcbStatus readResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockRead(FcbAddr, DtaAddr, requestedRecordCount); + (FcbStatus readResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockRead(FcbPointer, DtaAddr, requestedRecordCount); // Assert readResult.Should().Be(FcbStatus.Success); @@ -593,7 +601,7 @@ public void RandomBlockRead_MultipleRecords_AdvancesDtaForEachRecord() { _fixture.Memory.UInt8[DtaAddr + 8].Should().Be(0x43); // Cleanup - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); } [Fact] @@ -606,19 +614,19 @@ public void RandomBlockWrite_ZeroRecords_TruncatesFile() { } CreateTestFile("TRUNCATE.DAT", initialData); DosFileControlBlock fcb = CreateFcb("TRUNCATE", "DAT"); - _fixture.DosFcbManager.OpenFile(FcbAddr); + _fixture.DosFcbManager.OpenFile(FcbPointer); fcb.RecordSize = 10; fcb.RandomRecord = 5; ushort requestedRecordCount = 0; // Act - (FcbStatus writeResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockWrite(FcbAddr, DtaAddr, requestedRecordCount); + (FcbStatus writeResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockWrite(FcbPointer, DtaAddr, requestedRecordCount); // Assert writeResult.Should().Be(FcbStatus.Success); actualRecordCount.Should().Be(0); fcb.FileSize.Should().Be(50); - _fixture.DosFcbManager.CloseFile(FcbAddr); + _fixture.DosFcbManager.CloseFile(FcbPointer); new FileInfo(testFile).Length.Should().Be(50); } @@ -631,7 +639,7 @@ public void GetFileSize_UsesCeilingDivision() { fcb.RecordSize = 128; // Act - FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); @@ -647,7 +655,7 @@ public void GetFileSize_RecordSizeZero_UsesDefault128() { fcb.RecordSize = 0; // Act - FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); @@ -660,7 +668,7 @@ public void OpenFile_NonExistentFile_ReturnsError() { CreateFcb("NOTFOUND", "DAT"); // Act - FcbStatus result = _fixture.DosFcbManager.OpenFile(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.OpenFile(FcbPointer); // Assert: Should fail result.Should().Be(FcbStatus.Error); @@ -673,7 +681,7 @@ public void GetFileSize_NonExistentFile_ReturnsError() { fcb.RecordSize = 512; // Act - FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.GetFileSize(FcbPointer); // Assert result.Should().Be(FcbStatus.Error); @@ -686,7 +694,7 @@ public void FindFirst_WriteDtaInFcbFormat_DriveAndSpacePaddedName() { CreateFcb("HELLO", "TXT"); // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); @@ -707,7 +715,7 @@ public void FindFirst_FcbFormatDoesNotWriteAsciizAt0x1E() { CreateFcb("CHECK", "DAT"); // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); @@ -734,7 +742,7 @@ public void FindFirst_VolumeLabel_ReturnsDriveLabelInFcbFormat() { xfcb.FileExtension = "???"; // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); @@ -759,7 +767,7 @@ public void FindFirst_VolumeLabel_LongLabel_SplitsAcrossNameAndExtension() { xfcb.FileExtension = "???"; // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbAddr); + FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); // Assert result.Should().Be(FcbStatus.Success); From 838ee07022646ca934cd413ccf8f6275b325cecd Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:06:51 +0200 Subject: [PATCH 2/7] fix: FCB FindFirst now uses extended DTA format (FreeDOS, Civlization) Refactored DosFcbManager.FindFirst to delegate to the unified file search, matching FreeDOS behavior and populating the DTA in the standard extended format. Updated XML docs to clarify this. Rewrote and removed tests in DosFcbManagerTests to expect the extended DTA layout, including for volume label handling. --- .../Emulator/OperatingSystem/DosFcbManager.cs | 15 +++- tests/Spice86.Tests/Dos/DosFcbManagerTests.cs | 74 +++---------------- 2 files changed, 23 insertions(+), 66 deletions(-) diff --git a/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs b/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs index 723055e7be..2e94aa4414 100644 --- a/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs +++ b/src/Spice86.Core/Emulator/OperatingSystem/DosFcbManager.cs @@ -1165,17 +1165,24 @@ public FcbStatus RenameFile(SegmentedAddress fcbAddress) { /// /// INT 21h AH=11h FCB Find First using the FCB filename pattern. - /// Populates the DTA in FCB format (drive + 8-byte name + 3-byte ext + metadata). /// + /// + /// Matches FreeDOS FcbFindFirstNext: the FCB search delegates to the same + /// underlying find routine as the standard (extended) search rather than using a + /// separate FCB-specific DTA format. The XFCB attribute is forwarded to the + /// underlying find so volume-label/hidden/system searches work via Extended FCBs. + /// /// Segmented address of the FCB. /// describing the outcome. public FcbStatus FindFirst(SegmentedAddress fcbAddress) { - DosFileControlBlock fcb = GetFcb(fcbAddress, out byte attribute); + uint baseAddr = GetActualFcbBaseAddress(fcbAddress); + DosFileControlBlock fcb = new DosFileControlBlock(_memory, baseAddr); + GetFcb(fcbAddress, out byte attribute); string pattern = fcb.FullFileName; - DosFileOperationResult result = _dosFileManager.FcbFindFirstMatchingFile(pattern, attribute); + DosFileOperationResult result = _dosFileManager.FindFirstMatchingFile(pattern, attribute); FcbStatus status = result.IsError ? FcbStatus.Error : FcbStatus.Success; if (_loggerService.IsEnabled(LogEventLevel.Debug)) { - _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "FIND FIRST", fcb.BaseAddress, pattern, status); + _loggerService.Debug("FCB {Operation} at 0x{Address:X} ({Detail}) -> {Status}", "FIND FIRST", baseAddr, pattern, status); } return status; } diff --git a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs index 726ce139ff..91144afcf4 100644 --- a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs +++ b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs @@ -688,7 +688,7 @@ public void GetFileSize_NonExistentFile_ReturnsError() { } [Fact] - public void FindFirst_WriteDtaInFcbFormat_DriveAndSpacePaddedName() { + public void FindFirst_ExistingFile_PopulatesExtendedDtaFormat() { // Arrange CreateTestFile("HELLO.TXT", "content"); CreateFcb("HELLO", "TXT"); @@ -696,69 +696,21 @@ public void FindFirst_WriteDtaInFcbFormat_DriveAndSpacePaddedName() { // Act FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); - // Assert - result.Should().Be(FcbStatus.Success); - - uint dtaAddr = _fixture.DosFileManager.GetDiskTransferAreaPhysicalAddress(); - DosFileControlBlock dtaFcb = new DosFileControlBlock(_fixture.Memory, dtaAddr); - dtaFcb.DriveNumber.Should().NotBe(0, "FCB result should have a non-zero drive number"); - dtaFcb.FileName.Should().Be("HELLO ", "FCB filename must be 8 chars space-padded"); - dtaFcb.FileExtension.Should().Be("TXT", "FCB extension must be 3 chars space-padded"); - dtaFcb.FileSize.Should().Be(7, "file contains 'content' (7 bytes)"); - } - - [Fact] - public void FindFirst_FcbFormatDoesNotWriteAsciizAt0x1E() { - // Arrange: The extended DTA format writes ASCIIZ filename at offset 0x1E. - // FCB format should NOT use that layout. - CreateTestFile("CHECK.DAT", "data"); - CreateFcb("CHECK", "DAT"); - - // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); - - // Assert + // Assert: FCB FindFirst delegates to the unified DosFindFirst (FreeDOS behavior), + // so the DTA is populated in the standard extended format (ASCIIZ name at 0x1E). result.Should().Be(FcbStatus.Success); uint dtaAddr = _fixture.DosFileManager.GetDiskTransferAreaPhysicalAddress(); - - // In FCB format, offset 0x01 should be the filename. - // In extended format, offset 0x1E is the filename. Verify that offset 0x01 - // actually contains our file name (FCB format) rather than garbage. - DosFileControlBlock dtaFcb = new DosFileControlBlock(_fixture.Memory, dtaAddr); - string dtaName = dtaFcb.FileName.TrimEnd(); - dtaName.Should().Be("CHECK", "FCB FindFirst should write filename at FCB offset 0x01, not at extended offset 0x1E"); + DosDiskTransferArea dta = new DosDiskTransferArea(_fixture.Memory, dtaAddr); + dta.FileName.Should().Be("HELLO.TXT"); + dta.FileSize.Should().Be(7); } [Fact] - public void FindFirst_VolumeLabel_ReturnsDriveLabelInFcbFormat() { + public void FindFirst_VolumeLabel_ReturnsDriveLabelInExtendedFormat() { // Arrange: Create an Extended FCB with VolumeId attribute (0x08) - // to search for volume labels. - DosExtendedFileControlBlock xfcb = new DosExtendedFileControlBlock(_fixture.Memory, FcbAddr); - xfcb.Flag = 0xFF; - xfcb.Attribute = (byte)DosFileAttributes.VolumeId; - xfcb.DriveNumber = 0; - xfcb.FileName = "????????"; - xfcb.FileExtension = "???"; - - // Act - FcbStatus result = _fixture.DosFcbManager.FindFirst(FcbPointer); - - // Assert - result.Should().Be(FcbStatus.Success); - - uint dtaAddr = _fixture.DosFileManager.GetDiskTransferAreaPhysicalAddress(); - DosFileControlBlock dtaFcb = new DosFileControlBlock(_fixture.Memory, dtaAddr); - - // The default drive label is "Spice86" -> "SPICE86 " (8 chars) + " " (3 chars) - string fullLabel = (dtaFcb.FileName + dtaFcb.FileExtension); - fullLabel.TrimEnd().Should().Be("SPICE86", "FCB volume label should be the raw 11-byte label in FCB fields"); - } - - [Fact] - public void FindFirst_VolumeLabel_LongLabel_SplitsAcrossNameAndExtension() { - // Arrange: Set a label longer than 8 chars to verify it spans name+extension fields - _fixture.Dos.DosDriveManager.CurrentDrive.Label = "MYVOLUMEID"; + // to search for volume labels. FCB FindFirst forwards the XFCB attribute + // to the unified find, which populates the DTA in the standard format. DosExtendedFileControlBlock xfcb = new DosExtendedFileControlBlock(_fixture.Memory, FcbAddr); xfcb.Flag = 0xFF; xfcb.Attribute = (byte)DosFileAttributes.VolumeId; @@ -773,10 +725,8 @@ public void FindFirst_VolumeLabel_LongLabel_SplitsAcrossNameAndExtension() { result.Should().Be(FcbStatus.Success); uint dtaAddr = _fixture.DosFileManager.GetDiskTransferAreaPhysicalAddress(); - DosFileControlBlock dtaFcb = new DosFileControlBlock(_fixture.Memory, dtaAddr); - - // "MYVOLUMEID" -> 11 bytes padded: "MYVOLUME" + "ID " - dtaFcb.FileName.Should().Be("MYVOLUME", "First 8 chars of volume label in name field"); - dtaFcb.FileExtension.Should().Be("ID ", "Remaining chars of volume label in extension field"); + DosDiskTransferArea dta = new DosDiskTransferArea(_fixture.Memory, dtaAddr); + dta.FileAttributes.Should().Be((byte)DosFileAttributes.VolumeId); + dta.FileName.TrimEnd('\0', ' ', '.').Should().Contain("SPICE86"); } } \ No newline at end of file From c6baf7f9f41118be8e0244838e5d1d80f5e55a55 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:13:17 +0200 Subject: [PATCH 3/7] tests: Minor FCB tests cleanups --- tests/Spice86.Tests/Dos/DosFcbManagerTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs index 91144afcf4..9c2a414423 100644 --- a/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs +++ b/tests/Spice86.Tests/Dos/DosFcbManagerTests.cs @@ -10,7 +10,7 @@ using Xunit; -public class DosFcbManagerTests : IDisposable { +public sealed class DosFcbManagerTests : IDisposable { private const uint StringAddr = 0x1000; private const uint FcbAddr = 0x2000; private const uint DtaAddr = 0x3000; @@ -165,7 +165,7 @@ public void ParseFilename_SkipLeadingSeparators() { _fixture.Memory.SetZeroTerminatedString(StringAddr, " :;,=+ TEST.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.SkipLeadingSeparators | FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, _) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.SkipLeadingSeparators | FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -180,7 +180,7 @@ public void ParseFilename_WhitespaceAlwaysSkipped() { _fixture.Memory.SetZeroTerminatedString(StringAddr, " \t TEST.TXT", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, 0); + (FcbParseResult result, _) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, 0); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -241,7 +241,7 @@ public void ParseFilename_UppercaseConversion() { _fixture.Memory.SetZeroTerminatedString(StringAddr, "lowercase.ext", 128); // Act - (FcbParseResult result, uint bytesAdvanced) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); + (FcbParseResult result, _) = _fixture.DosFcbManager.ParseFilename(StringPointer, FcbPointer, FcbParseControl.LeaveDriveUnchanged); // Assert result.Should().Be(FcbParseResult.NoWildcards); @@ -588,7 +588,7 @@ public void RandomBlockRead_MultipleRecords_AdvancesDtaForEachRecord() { _fixture.DosFcbManager.OpenFile(FcbPointer); fcb.RecordSize = 4; fcb.RandomRecord = 0; - ushort requestedRecordCount = 3; + const ushort requestedRecordCount = 3; // Act (FcbStatus readResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockRead(FcbPointer, DtaAddr, requestedRecordCount); @@ -617,7 +617,7 @@ public void RandomBlockWrite_ZeroRecords_TruncatesFile() { _fixture.DosFcbManager.OpenFile(FcbPointer); fcb.RecordSize = 10; fcb.RandomRecord = 5; - ushort requestedRecordCount = 0; + const ushort requestedRecordCount = 0; // Act (FcbStatus writeResult, ushort actualRecordCount) = _fixture.DosFcbManager.RandomBlockWrite(FcbPointer, DtaAddr, requestedRecordCount); From 618a4c341baa9a8dd62b0eab98af4c1dd25d4c1d Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:14:08 +0200 Subject: [PATCH 4/7] chore: Bump version to 14 and update release notes URL (CPU, DOS, Debugger) Updated the project version from 13.1.2 to 14 in Directory.Build.props. Also changed the package release notes link to reference the v14 release notes. --- src/Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e22fd6843b..e81d4f53c2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -16,10 +16,10 @@ - 13.1.2 + 14 Alberto Marnetto, Ales Teska, Artjom Vejsel, Edu Garcia, Ivan Kuzmenko, John Källén, Joris van Eijden, Karl Lenz, Kevin Ferrare, LowLevelMahn, Maximilien Noal, Stefan Hueg From 8d9d80f46759f4b708e5c0193ef250629f597888 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:36:55 +0200 Subject: [PATCH 5/7] chore: Document I/O port handler system and improve docs UX Added "Quick navigation" to README and documented the new I/O port handler system in both README.md and index.html. Introduced a dedicated "I/O Port Handlers" section with usage instructions, a summary table, and callouts. Updated index.html with new callout styles and enhanced navigation, including quick start and compatibility tips. --- README.md | 28 +++++++++++++++++++++++++ docs/index.html | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/README.md b/README.md index 9515429aca..29b040f6eb 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,18 @@ NOTE: This is a port, and a continuation from the [original Java Spice86](https: It requires [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) and runs on Windows, macOS, and Linux. +## Quick navigation + +- [Approach](#approach) +- [Running your exe](#running-your-exe) +- [Command line options](#command-line-options) +- [I/O port handlers (replacing IOPortHandler)](#io-port-handlers-replacing-ioporthandler) +- [Dynamic analysis](#dynamic-analysis) +- [Overriding emulated code with C# code](#overriding-emulated-code-with-c-code) +- [HTTP server](#http-server) +- [How to build on your machine](#how-to-build-on-your-machine) +- [Some screenshots](#some-screenshots) + ## Approach Rewriting a program from only the binary is a hard task. @@ -148,6 +160,22 @@ Spice86 -e program.exe --CpuHeavyLog \ - OplMode: OPL synthesis mode. Values: None, Opl2, DualOpl2, Opl3, Opl3Gold. Default is Opl3. - SbMixer: Enable Sound Blaster mixer control of OPL voices. Default is true. +## I/O port handlers (replacing IOPortHandler) + +Spice86 routes emulated hardware port I/O through the dispatcher / handler system: + +- `IIOPortHandler`: contract implemented by a device that handles one or more I/O ports. +- `IOPortDispatcher`: central router that maps ports to handlers. +- `DefaultIOPortHandler`: fallback behavior for unhandled ports. + +If you need to replace an `IOPortHandler` implementation in your integration: + +1. Implement `IIOPortHandler` in your device class. +2. Register your handler for the port range in the dispatcher wiring. +3. Keep `--FailOnUnhandledPort` enabled when validating to catch missing routes early. + +This makes it easier to incrementally swap hardware behavior while keeping unsupported ports visible during reverse engineering. + ## Dynamic analysis Spice86 speaks the [GDB](https://www.gnu.org/software/gdb/) remote protocol: diff --git a/docs/index.html b/docs/index.html index 23f72a7f2b..2584b0c055 100644 --- a/docs/index.html +++ b/docs/index.html @@ -20,6 +20,9 @@ --code-bg: #1a2236; --warn: #d29922; --danger: #f85149; + --info-bg: rgba(88,166,255,.12); + --success-bg: rgba(63,185,80,.12); + --warning-bg: rgba(210,153,34,.14); --nav-width: 240px; } @@ -314,6 +317,23 @@ } .callout strong { color: var(--warn); } + .callout-info { + border-left-color: var(--accent); + background: var(--info-bg); + } + .callout-info strong { color: var(--accent); } + + .callout-success { + border-left-color: var(--accent2); + background: var(--success-bg); + } + .callout-success strong { color: var(--accent2); } + + .callout-warning { + border-left-color: var(--warn); + background: var(--warning-bg); + } + /* ── Mobile nav toggle ── */ .mobile-header { display: none; } .mobile-overlay { @@ -397,6 +417,7 @@

Spice86

  • CLI Options
  • +
  • I/O Port Handlers
  • GDB Integration
  • Built-in Debugger
  • Code Overrides
  • @@ -445,6 +466,10 @@

    Spice86

    Debugger Wiki Example project: Cryogenic + +
    + Quick start: Run Spice86 -e game.exe, inspect with GDB on :10000, then incrementally replace assembly with C# overrides. +
    @@ -736,6 +761,33 @@

    Logging

    + +
    +

    I/O Port Handlers

    +

    + Spice86 routes hardware port reads and writes through a dispatcher/handler model. + This keeps devices isolated and makes hardware behavior easier to swap during + reverse engineering. +

    + +
    + + + + + + + + + +
    ComponentRole
    IIOPortHandlerInterface implemented by hardware device handlers.
    IOPortDispatcherRoutes I/O port operations to the correct handler.
    DefaultIOPortHandlerFallback behavior when no specific handler is registered.
    +
    + +
    + Replacing IOPortHandler: Implement IIOPortHandler, register it in the dispatcher wiring, and run with --FailOnUnhandledPort true to catch missing routes early. +
    +
    +

    GDB Integration

    @@ -1020,6 +1072,9 @@

    Game Compatibility

    This list reflects the state as of early 2026. A program crashing usually indicates an unimplemented BIOS/DOS interrupt or a video mode that has not been implemented.

    +
    + Reading this table: Use it as a quick snapshot. For the latest status and detailed notes, prefer COMPATIBILITY.md. +
    From be832d935d21bee9064c58638253ff5691f7b20a Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:39:31 +0200 Subject: [PATCH 6/7] chore: Vastly expand MCP.MD document --- doc/mcp.md | 413 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 352 insertions(+), 61 deletions(-) diff --git a/doc/mcp.md b/doc/mcp.md index 474bb8dfaf..bbf6333f78 100644 --- a/doc/mcp.md +++ b/doc/mcp.md @@ -1,105 +1,396 @@ -# Spice86 MCP HTTP server +# Spice86 MCP HTTP Server ## Overview -Spice86 exposes a Model Context Protocol (MCP) server over HTTP. -It can be used to inspect emulator state and drive execution through structured tool calls. +Spice86 exposes a **Model Context Protocol (MCP) server** over HTTP, enabling structured programmatic control of the emulator. AI clients, automation scripts, and external tooling can use MCP to inspect, manipulate, and drive execution of DOS programs running in Spice86. + +This server provides **65+ built-in tools** covering: +- CPU state inspection (registers, flags, instruction pointer) +- Memory read/write/search/disassembly +- I/O port read/write +- Execution control (pause, resume, step, step-over) +- Breakpoint management (execution, memory read/write) +- Function discovery and CFG (Control Flow Graph) traversal +- Video state inspection and screenshot capture +- Sound device state (SoundBlaster, OPL, MIDI, PC Speaker) +- DOS structures (PSP, MCB, File handle table) +- EMS and XMS memory management + +The HTTP transport is **stateless** by default to maximize compatibility with real-world AI clients that may skip session negotiation or reuse connection state unpredictably. + +--- ## Endpoints -- MCP endpoint: :/mcp -- Health endpoint: :/health +| Endpoint | Description | +|----------|-------------| +| `http://localhost:/mcp` | MCP protocol endpoint (JSON-RPC over HTTP) | +| `http://localhost:/health` | Health check endpoint returning `{"status":"ok"}` | + +**Default port:** `8081` + +**CLI option:** `--mcp-http-port ` -Default port is 8081. -Use --mcp-http-port to change it. +--- -## Quick start +## Quick Start -Start Spice86 with MCP enabled (enabled by default): +### 1. Start Spice86 with MCP enabled (enabled by default) ```bash Spice86 -e program.exe --mcp-http-port 8081 ``` -Then connect your MCP client to: +### 2. Connect your MCP client -- +Point your MCP client to: +``` +http://localhost:8081/mcp +``` -## Protocol behavior +### 3. Discover available tools + +```json +POST /mcp +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { "name": "MyClient", "version": "1.0" } + } +} +``` -- Transport mode is stateless. -- The server does not issue Mcp-Session-Id. -- Standard MCP flow is supported: initialize, tools/list, tools/call. +Then call `tools/list` to enumerate all available tools, or call `mcp_about` for high-level capability metadata. -## Tooling scope +--- -The built-in toolset covers emulator-oriented operations such as: +## Protocol Behavior -- CPU and execution state inspection -- Memory read/write/search -- Pause, resume, step, and step-over control -- Breakpoint management -- Video state and screenshot access -- EMS and XMS inspection helpers -- Input automation helpers +- **Transport:** HTTP (stateless by default) +- **Protocol:** JSON-RPC 2.0 +- **Session management:** The server does **not** issue `Mcp-Session-Id` headers by default (`options.Stateless = true`). This prevents 404 errors when AI clients reuse session IDs from fresh TCP connections or skip the `notifications/initialized` handshake. +- **Standard MCP flow:** `initialize` → `tools/list` → `tools/call` + +**Stateless mode** is recommended for AI agents and automated workflows. If you need stateful sessions (e.g., for long-lived interactive clients), you can modify `McpHttpHost.cs` to set `options.Stateless = false`. + +--- + +## Built-in Tool Categories + +Spice86 ships with **65+ MCP tools** organized into these capability scopes: + +### CPU State & Execution Control +- `read_cpu_state`: Read all general-purpose registers, segment registers, IP, flags, and cycle count +- `pause`: Pause emulation +- `resume`: Resume emulation +- `step`: Execute one instruction and pause +- `step_over`: Step over CALL instructions (run until RET) +- `get_pause_status`: Check if emulator is paused + +### Memory Operations +- `read_memory`: Read a memory range (segment, offset, length) as hex bytes +- `write_memory`: Write hex bytes to memory +- `search_memory`: Search for a hex pattern in conventional RAM +- `read_disassembly`: Disassemble instructions at a given address + +### I/O Ports +- `read_io_port`: Read from an I/O port +- `write_io_port`: Write to an I/O port + +### Breakpoints +- `add_execution_breakpoint`: Break when CS:IP reaches an address +- `add_memory_read_breakpoint`: Break on memory read +- `add_memory_write_breakpoint`: Break on memory write +- `remove_execution_breakpoint` +- `remove_memory_read_breakpoint` +- `remove_memory_write_breakpoint` +- `list_execution_breakpoints` +- `list_memory_breakpoints` + +### Functions & CFG +- `list_functions`: List discovered functions sorted by call count +- `read_cfg_cpu_graph`: Dump the Control Flow Graph (CFG) built by `CfgCpu` + +### Video +- `read_video_state`: Current video mode, resolution, text/graphics flag, cursor position +- `read_video_state_detailed`: Full VGA register dump +- `video_set_mode`: Change video mode +- `read_vga_memory`: Read VGA VRAM +- `write_vga_memory`: Write VGA VRAM +- `vga_set_palette_entry`: Set VGA palette color +- `capture_screenshot`: Save a screenshot to disk and return the path + +### Sound Devices +- **SoundBlaster:** `read_sound_blaster_state`, `sound_blaster_set_speaker`, `read_sound_blaster_dsp_version`, `sound_blaster_write_mixer_register`, etc. +- **OPL (Adlib/SB OPL2/OPL3):** `read_opl_state`, `opl_write_register` +- **PC Speaker:** `read_pc_speaker_state`, `pc_speaker_set_control` +- **MIDI:** `read_midi_state`, `midi_reset`, `midi_enter_uart_mode`, `midi_send_bytes` + +### Input Automation +- `send_keyboard_key`: Send a keystroke (press + release or hold) +- `send_mouse_packet`: Send raw mouse data packet +- `send_mouse_move`: Move mouse cursor +- `send_mouse_button`: Press/release mouse button + +### DOS & BIOS Structures +- `read_dos_psp`: Read the DOS Program Segment Prefix +- `read_dos_mcb_chain`: Dump the DOS Memory Control Block chain +- `read_dos_file_handle_table`: List open file handles +- `read_dos_current_directory`: Get current drive and directory path +- `read_bios_equipment_word`: Read BIOS equipment flags + +### EMS & XMS +- `read_ems_state`: EMS handle allocation, page frame mapping +- `read_xms_state`: XMS handle allocation, HMA usage +- `read_ems_page_frame`: Dump EMS page frame content +- `read_xms_block`: Read an XMS memory block + +### Metadata & Diagnostics +- `mcp_about`: High-level server metadata, capability scopes, extension points, tool count + +For a complete tool list with parameter details, call `tools/list` via the MCP endpoint. + +--- + +## Tool Invocation & Auto-Pause + +Most tools automatically **pause** the emulator before execution and **resume** after. This ensures consistent state during inspection and prevents race conditions. A few tools (marked with `[McpManualControl]`) skip auto-pause and require the client to explicitly call `pause` if needed. + +**Example: Reading CPU state** + +```json +POST /mcp +Content-Type: application/json + +{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "read_cpu_state", + "arguments": {} + } +} +``` + +**Response:** + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "content": [ + { + "type": "resource", + "resource": { + "uri": "data:application/json;base64,eyJFQVgiOjEyMywgIkVCWCI6NDU2LCAuLi59" + } + } + ] + } +} +``` -For AI/client discoverability, the built-in `mcp_about` tool returns concise structured metadata: +The structured content (JSON) is base64-encoded in the resource field. + +**Example: Writing memory** + +```json +POST /mcp +{ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": { + "name": "write_memory", + "arguments": { + "segment": 4096, + "offset": 256, + "data": "B80200CD21" + } + } +} +``` -- server purpose and version -- capability scopes -- stateless transport flag -- endpoint paths -- extension points for project-specific tools -- recommended discovery sequence (`initialize`, `tools/list`, `mcp_about`) +--- -## Extending MCP from user projects +## Extending MCP from User Projects -Spice86 supports external MCP tool registration. +Spice86 supports **external MCP tool registration**. You can add project-specific tools for game-specific operations (e.g., reading player stats, manipulating inventory, triggering events). -McpHttpHost.Start accepts: +### Extension Model -- additionalToolAssemblies -- additionalServices +1. **Implement `IMcpToolSupplier`** in your project to provide custom tool assemblies and injectable services. +2. **Mark your tool classes with `[McpServerToolType]`** and individual methods with `[McpServerTool(Name = "...")]`. +3. **Register your tools at startup** by passing additional assemblies and services to `McpHttpHost.Start`. -Additional assemblies are scanned for classes marked with McpServerToolType. -Additional services are registered for constructor injection in those tool classes. +### Extension Entry Points -### Minimal setup +| Method | Purpose | +|--------|---------| +| `IMcpToolSupplier.GetMcpToolAssemblies()` | Return additional assemblies containing `[McpServerToolType]` classes | +| `IMcpToolSupplier.GetMcpServices()` | Return injectable services used by custom tools | +| `McpHttpHost.Start(additionalToolAssemblies, additionalServices)` | Register external tools at server start | + +### Minimal Extension Example ```csharp using Spice86.Core.Emulator.Mcp; +using ModelContextProtocol.Server; + +// 1. Implement IMcpToolSupplier in your override project +public class MyGameMcpToolSupplier : IMcpToolSupplier { + public IEnumerable GetMcpToolAssemblies() { + return [typeof(MyGameMcpTools).Assembly]; + } + + public IEnumerable GetMcpServices() { + return [new MyGameContext(...)]; + } +} + +// 2. Define your custom tools +[McpServerToolType] +public sealed class MyGameMcpTools { + private readonly MyGameContext _context; + + public MyGameMcpTools(MyGameContext context, EmulatorMcpServices emulatorServices) { + _context = context; + } + + [McpServerTool(Name = "read_player_health", UseStructuredContent = true)] + public object ReadPlayerHealth() { + // Read from memory via _context or emulatorServices + int health = _context.GetPlayerHealth(); + return new { Health = health, MaxHealth = 100 }; + } + [McpServerTool(Name = "set_player_gold", UseStructuredContent = true)] + public object SetPlayerGold(int amount) { + _context.SetPlayerGold(amount); + return new { Success = true, Gold = amount }; + } +} +``` + +### Registering Custom Tools at Startup + +Modify your startup wiring (typically in `Spice86DependencyInjection.cs` or a custom entry point): + +```csharp +IMcpToolSupplier supplier = new MyGameMcpToolSupplier(); McpHttpHost host = new(loggerService); host.Start( services: emulatorMcpServices, port: configuration.McpHttpPort, - additionalToolAssemblies: [typeof(MyProjectMcpTools).Assembly], - additionalServices: [new MyProjectMcpContext(...)]); + additionalToolAssemblies: supplier.GetMcpToolAssemblies(), + additionalServices: supplier.GetMcpServices() +); ``` -```csharp -using ModelContextProtocol.Server; +**Note:** The default `Spice86` startup does **not** load external tool assemblies. You must wire them explicitly if you want project-specific tools. -[McpServerToolType] -public sealed class MyProjectMcpTools { - [McpServerTool(Name = "my_project_ping", UseStructuredContent = true)] - public object Ping() { - return new { success = true, message = "pong" }; - } -} -``` +--- + +## Practical Guidance for Extension Authors + +### Tool Design Best Practices + +1. **Keep tools deterministic:** Avoid relying on global mutable state outside the emulator. +2. **Prefer semantic tools:** Expose high-level operations (e.g., `read_player_inventory`) instead of raw memory offsets. +3. **Keep low-level tools available:** Also provide `read_memory_at_player_inventory_address` for diagnostics when the semantic tool breaks. +4. **Return compact structured payloads:** Avoid dumping large arrays unless necessary. Use pagination or limits. +5. **Add integration tests:** Write real MCP `tools/call` tests that verify your tools work end-to-end. + +### Debugging Extension Tools + +- **Check the MCP log:** `logs/mcp.log` contains startup and invocation errors. +- **Verify tool registration:** Call `tools/list` and ensure your custom tools appear. +- **Test auto-pause behavior:** If your tool accesses emulator state, ensure it pauses correctly or mark it with `[McpManualControl]`. + +--- + +## Common Use Cases + +### AI-Driven Reverse Engineering + +An AI agent can: +1. Call `read_cpu_state` to see where the program is stuck. +2. Call `read_disassembly` to inspect the next 10 instructions. +3. Call `search_memory` to find a string or data pattern. +4. Call `add_execution_breakpoint` to pause at a suspect function. +5. Call `list_functions` to see which functions are called most often. +6. Call `capture_screenshot` to see the current video output. + +### Automated Testing + +A test script can: +1. Call `pause` to halt execution. +2. Call `write_memory` to inject test data. +3. Call `resume` and wait for a breakpoint. +4. Call `read_dos_file_handle_table` to verify the program opened the expected file. +5. Call `capture_screenshot` and compare against a baseline image. + +### Game Trainer / Cheat Tool + +A trainer tool can: +1. Call `read_dos_psp` to locate the game's data segment. +2. Call `search_memory` to find the player's health value. +3. Call `write_memory` to set health to max. +4. Call `add_memory_write_breakpoint` to detect when the game decrements health. + +### Live Debugging Dashboard + +A web dashboard can: +1. Poll `read_cpu_state` every 500ms to display registers. +2. Call `read_video_state` to show current video mode. +3. Call `read_sound_blaster_state` to visualize audio channels. +4. Call `list_functions` to show a live call-count heatmap. + +--- + +## Logs & Troubleshooting + +- **MCP server log:** `logs/mcp.log` (warning level by default) +- **Emulator logs:** Console or file (controlled by `--VerboseLogs`, `--WarningLogs`, `--SilencedLogs`) +- **Health check:** `GET http://localhost:8081/health` should return `{"status":"ok","service":"Spice86 MCP Server"}` + +**Common issues:** + +| Problem | Solution | +|---------|----------| +| Client gets 404 on `/mcp` | Check that `--mcp-http-port` is set and the server started successfully | +| Tools return "Tool disabled" error | Some tools may be disabled if the emulator is not in the expected state (e.g., video tools when no VGA card is initialized) | +| Tools time out | If emulator is not responding to pause requests, ensure the emulation loop is running and not deadlocked | +| Client skips session ID and gets 404 | Ensure stateless mode is enabled (default); if using stateful mode, ensure client sends `Mcp-Session-Id` header | + +--- + +## Reference -## Important startup note +### Related Files -Current default Spice86 startup initializes MCP with built-in tools only. -In the default dependency wiring, McpHttpHost.Start is called without additional assemblies or services. +- `Spice86.Core/Emulator/Mcp/McpHttpHost.cs` - HTTP server setup and lifecycle +- `Spice86.Core/Emulator/Mcp/EmulatorMcpTools.cs` - Built-in tool implementations +- `Spice86.Core/Emulator/Mcp/EmulatorMcpServices.cs` - Injected services for built-in tools +- `Spice86.Core/Emulator/Mcp/IMcpToolSupplier.cs` - Extension interface for custom tools +- `Spice86.Core/Emulator/Mcp/Response/McpAboutResponse.cs` - Metadata response structure -To expose project-specific tools, use a custom startup path (or patch startup wiring) and pass your extension assemblies and services. +### Further Reading -## Practical guidance for extension authors +- [Model Context Protocol Specification](https://modelcontextprotocol.io/) +- [Spice86 CFG CPU Documentation](cfgcpuReadme.md) +- [Spice86 Internal Debugger Wiki](https://github.com/OpenRakis/Spice86/wiki/Spice86-internal-debugger) +- [Cryogenic Project (MCP Extension Example)](https://github.com/OpenRakis/Cryogenic) -- Keep tool behavior deterministic. -- Prefer semantic tools for common operations. -- Keep low-level tools available for fallback diagnostics. -- Return compact structured payloads. -- Add integration tests that execute real MCP tools/calls. +**Quick links:** +- Health check: `http://localhost:8081/health` +- MCP endpoint: `http://localhost:8081/mcp` +- Tool discovery: Call `tools/list` or `mcp_about` From 6e453b9e580b699dd12b17cfd4f74eb508833866 Mon Sep 17 00:00:00 2001 From: Maximilien Noal Date: Sun, 17 May 2026 11:41:20 +0200 Subject: [PATCH 7/7] Revert "chore: Document I/O port handler system and improve docs UX" This reverts commit 8d9d80f46759f4b708e5c0193ef250629f597888. --- README.md | 28 ------------------------- docs/index.html | 55 ------------------------------------------------- 2 files changed, 83 deletions(-) diff --git a/README.md b/README.md index 29b040f6eb..9515429aca 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,6 @@ NOTE: This is a port, and a continuation from the [original Java Spice86](https: It requires [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) and runs on Windows, macOS, and Linux. -## Quick navigation - -- [Approach](#approach) -- [Running your exe](#running-your-exe) -- [Command line options](#command-line-options) -- [I/O port handlers (replacing IOPortHandler)](#io-port-handlers-replacing-ioporthandler) -- [Dynamic analysis](#dynamic-analysis) -- [Overriding emulated code with C# code](#overriding-emulated-code-with-c-code) -- [HTTP server](#http-server) -- [How to build on your machine](#how-to-build-on-your-machine) -- [Some screenshots](#some-screenshots) - ## Approach Rewriting a program from only the binary is a hard task. @@ -160,22 +148,6 @@ Spice86 -e program.exe --CpuHeavyLog \ - OplMode: OPL synthesis mode. Values: None, Opl2, DualOpl2, Opl3, Opl3Gold. Default is Opl3. - SbMixer: Enable Sound Blaster mixer control of OPL voices. Default is true. -## I/O port handlers (replacing IOPortHandler) - -Spice86 routes emulated hardware port I/O through the dispatcher / handler system: - -- `IIOPortHandler`: contract implemented by a device that handles one or more I/O ports. -- `IOPortDispatcher`: central router that maps ports to handlers. -- `DefaultIOPortHandler`: fallback behavior for unhandled ports. - -If you need to replace an `IOPortHandler` implementation in your integration: - -1. Implement `IIOPortHandler` in your device class. -2. Register your handler for the port range in the dispatcher wiring. -3. Keep `--FailOnUnhandledPort` enabled when validating to catch missing routes early. - -This makes it easier to incrementally swap hardware behavior while keeping unsupported ports visible during reverse engineering. - ## Dynamic analysis Spice86 speaks the [GDB](https://www.gnu.org/software/gdb/) remote protocol: diff --git a/docs/index.html b/docs/index.html index 2584b0c055..23f72a7f2b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -20,9 +20,6 @@ --code-bg: #1a2236; --warn: #d29922; --danger: #f85149; - --info-bg: rgba(88,166,255,.12); - --success-bg: rgba(63,185,80,.12); - --warning-bg: rgba(210,153,34,.14); --nav-width: 240px; } @@ -317,23 +314,6 @@ } .callout strong { color: var(--warn); } - .callout-info { - border-left-color: var(--accent); - background: var(--info-bg); - } - .callout-info strong { color: var(--accent); } - - .callout-success { - border-left-color: var(--accent2); - background: var(--success-bg); - } - .callout-success strong { color: var(--accent2); } - - .callout-warning { - border-left-color: var(--warn); - background: var(--warning-bg); - } - /* ── Mobile nav toggle ── */ .mobile-header { display: none; } .mobile-overlay { @@ -417,7 +397,6 @@

    Spice86

    - - - - - - - - -
    ComponentRole
    IIOPortHandlerInterface implemented by hardware device handlers.
    IOPortDispatcherRoutes I/O port operations to the correct handler.
    DefaultIOPortHandlerFallback behavior when no specific handler is registered.
    -
    - -
    - Replacing IOPortHandler: Implement IIOPortHandler, register it in the dispatcher wiring, and run with --FailOnUnhandledPort true to catch missing routes early. -
    -
    -

    GDB Integration

    @@ -1072,9 +1020,6 @@

    Game Compatibility

    This list reflects the state as of early 2026. A program crashing usually indicates an unimplemented BIOS/DOS interrupt or a video mode that has not been implemented.

    -
    - Reading this table: Use it as a quick snapshot. For the latest status and detailed notes, prefer COMPATIBILITY.md. -