diff --git a/AquaCubeIT.Service.NetFloppy/.dockerignore b/AquaCubeIT.Service.NetFloppy/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj new file mode 100644 index 0000000..125f4c9 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/AquaCubeIT.Service.NetFloppy.DataAccess.csproj @@ -0,0 +1,9 @@ + + + + net9.0 + enable + enable + + + diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs new file mode 100644 index 0000000..46f63f9 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Class1.cs @@ -0,0 +1,7 @@ +namespace AquaCubeIT.Service.NetFloppy.DataAccess +{ + public class Class1 + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs new file mode 100644 index 0000000..3693884 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaBlock.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models +{ + public class MediaBlock + { + public Guid Id { get; set; } + public required MediaImage MediaImage { get; set; } + public long BlockId { get; set; } + public byte[] Data { get; set; } + public DateTime DateStamp { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs new file mode 100644 index 0000000..4712b3e --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaImage.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models +{ + public class MediaImage + { + public Guid Id { get; set; } + public required string Name { get; set; } + public string? Description { get; set; } + public required MediaType MediaType { get; set; } + public long SizeInBytes { get; set; } + public int Hash { get; set; } + public long NoOfBlocks { get; set; } + public long BlockSize { get; set; } + public DateTime DateStamp { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs new file mode 100644 index 0000000..2c3e7df --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.DataAccess/Models/MediaType.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AquaCubeIT.Service.NetFloppy.DataAccess.Models +{ + /// + /// Media type of the stored data image + /// + public class MediaType + { + public Guid Id { get; set; } + public required string Name { get; set; } + public string? Description { get; set; } + public DateTime DateStamp { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln new file mode 100644 index 0000000..7ad24c8 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35828.75 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaCubeIT.Service.NetFloppy", "AquaCubeIT.Service.NetFloppy\AquaCubeIT.Service.NetFloppy.csproj", "{BBCB4FB7-219C-49D4-9F20-C65189D820BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AquaCubeIT.Service.NetFloppy.DataAccess", "AquaCubeIT.Service.NetFloppy.DataAccess\AquaCubeIT.Service.NetFloppy.DataAccess.csproj", "{883D7BF4-EC7F-436F-AE3A-5BDE9E024801}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BBCB4FB7-219C-49D4-9F20-C65189D820BD}.Release|Any CPU.Build.0 = Release|Any CPU + {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Release|Any CPU.ActiveCfg = Release|Any CPU + {883D7BF4-EC7F-436F-AE3A-5BDE9E024801}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BE3474A5-542F-4060-AE03-C0FB9EBA0A0E} + EndGlobalSection +EndGlobal diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj new file mode 100644 index 0000000..aae4f4f --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj @@ -0,0 +1,21 @@ + + + + net9.0 + enable + enable + f3d90137-247e-42ea-b274-3bbba8be89c4 + Linux + + + + + + + + + + + + + diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user new file mode 100644 index 0000000..983ecfc --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http new file mode 100644 index 0000000..c855c4d --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.http @@ -0,0 +1,9 @@ +@AquaCubeIT.Service.NetFloppy_HostAddress = http://localhost:9000 + +GET {{AquaCubeIT.Service.NetFloppy_HostAddress}}/register/ +Accept: application/json + +GET {{AquaCubeIT.Service.NetFloppy_HostAddress}}/logon/ +Accept: application/json + +### diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs new file mode 100644 index 0000000..d75a939 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/LogonController.cs @@ -0,0 +1,177 @@ +using AquaCubeIT.Service.NetFloppy.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity.Data; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Sockets; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using RefreshRequest = AquaCubeIT.Service.NetFloppy.Models.RefreshRequest; + +namespace AquaCubeIT.Service.NetFloppy.Controllers; + +[ApiController] +[Route("[controller]")] +public class LogonController : ControllerBase +{ + private readonly IConfiguration _config; + private readonly ILogger _logger; + + public LogonController(ILogger logger, IConfiguration config) + { + _config = config; + _logger = logger; + } + + [HttpGet] + [Authorize] + public async Task Get() + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + var email = User.FindFirstValue(ClaimTypes.Email); + var roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToList(); + + var allClaims = User.Claims.Select(c => c.Value).ToList(); + + return $"User: {userId}, {email}, {string.Join(",", roles)}, {string.Join(",",allClaims)}"; + } + + private RefreshResponsePayload GenerateToken(RefreshRequestPayload request) + { + + if (request.RefreshToken == null) + { + throw new BadHttpRequestException("Bad Payload"); + } + + // Hash incoming token the same way + byte[] raw = Convert.FromBase64String(request.RefreshToken); + string reqHash = Convert.ToHexString(SHA256.HashData(raw)); + + // Get token by hash + + + + // Check family is ok + + string? family = null; + var userName = "Test"; + + // Check if revoked + + + + // Throw error if revoked or expired kill all family tokens + + + + // Save refresh token by hash + + // Check + return GenerateToken(userName, GenerateRefreshToken(family)); + } + + private string GenerateRefreshToken(string? family) + { + // Update refreshToken + var bytes = RandomNumberGenerator.GetBytes(64); + var refreshToken = Convert.ToBase64String(bytes); + var hash = Convert.ToHexString(SHA256.HashData(bytes)); + family = family ?? Guid.NewGuid().ToString("N"); + + // Save refresh token by hash + + return refreshToken; + } + + private LogonResponsePayload GenerateToken(LogonRequestPayload request) + { + return GenerateToken(request.UserName, GenerateRefreshToken(null)); + } + + private RefreshResponsePayload GenerateToken(string? userName, string refreshToken) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, userName), + new Claim(ClaimTypes.Name, "Stephen Barriball"), + new Claim(ClaimTypes.Role, "User") + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var expiryDate = DateTime.Now.AddMinutes(15); + var token = new JwtSecurityToken( + issuer: _config["Jwt:Issuer"], + audience: _config["Jwt:Audience"], + claims: claims, + expires: expiryDate, + signingCredentials: creds); + + // Return new token and refresh token + return new RefreshResponsePayload() { Token = new JwtSecurityTokenHandler().WriteToken(token), RefreshToken = refreshToken, TokenExpiry = expiryDate }; + } + + + [HttpPost] + public async Task Post(LogonRequest request) + { + var response = new LogonResponse(); + + if (request.Payload == null) + { + throw new Exception("Invalid Payload"); + } + + // Validate user here + if (request.Payload.UserName == "test" && request.Payload.Password == "password") + { + response.Payload = GenerateToken(request.Payload); + response.Success = true; + } + else + { + response.Success = false; + response.Error = new ErrorResponsePayload() { ErrorNo = 15, Message = "Invalid Login" }; + } + + return response; + } + + [HttpPost("refresh")] + public async Task Refresh(RefreshRequest request) + { + try + { + if (request.Payload == null) + { + throw new BadHttpRequestException("Invalid Payload"); + } + + // Hash incoming token the same way + byte[] raw = Convert.FromBase64String(request.Payload.RefreshToken); + string reqHash = Convert.ToHexString(SHA256.HashData(raw)); + + //var stored = await _repo.GetRefreshTokenByHashAsync(reqHash); + + //if (stored is null || stored.RevokedAtUtc != null || stored.ExpiresAtUtc < DateTime.UtcNow) + //{ + // throw new UnauthorizedAccessException(); + //return Unauthorized(); + //} + + // Rotate: revoke old, issue new pair in same family + //stored.RevokedAtUtc = DateTime.UtcNow; + + //var user = await _users.GetByIdAsync(stored.UserId); + + return new RefreshResponse() { Payload = GenerateToken(request.Payload), Success = true }; + } + catch(Exception e) + { + return new RefreshResponse() { Success = false, Error = new ErrorResponsePayload() { Exception = e, Message = e.Message, ErrorNo = 14 } }; + } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs new file mode 100644 index 0000000..d1edbfd --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/OperationsController.cs @@ -0,0 +1,174 @@ +using AquaCubeIT.Service.NetFloppy.Models; +using Fat16Lib; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using System; +using System.IO; +using System.Text; + +namespace AquaCubeIT.Service.NetFloppy.Controllers; + +[ApiController] +[Route("[controller]")] +public class OperationsController : ControllerBase +{ + private readonly ILogger _logger; + private const int DISK_SIZE_MB = 5; + private const int DISK_SECTOR_SIZE = 512; // 512 bytes/sector + private const int DISK_SECTOR_COUNT = DISK_SIZE_MB * 1024 * 1024 / DISK_SECTOR_SIZE; // ~2 MB (4000 * 512) + private const long DISK_BYTE_SIZE = DISK_SECTOR_COUNT * DISK_SECTOR_SIZE; + + public OperationsController(ILogger logger) + { + _logger = logger; + } + + private ErrorResponsePayload GetExceptionPayload(Exception exception) + { + return new ErrorResponsePayload() { ErrorNo = 16, Message = exception.Message, Exception = exception }; + } + + [Authorize(Policy = "MustBeUser")] + [HttpPost("LoadImage")] + public async Task LoadImage(LoadImageRequest request) + { + try + { + // Check image exists + + + // Check permissions of user + + + + // Get image data + byte[] data = null; + + if (System.IO.File.Exists("c:\\temp\\test_image1.img")) + { + + data = await System.IO.File.ReadAllBytesAsync("c:\\temp\\test_image1.img"); + } + else + { + var img = new Fat16Image(new Fat16ImageOptions + { + TotalSizeBytes = DISK_BYTE_SIZE, + VolumeLabel = "NetFloppy" + }); + + img.AddFile("README.TXT", Encoding.ASCII.GetBytes("Welcome to NetFloppy\r\n")); + img.AddFile("FF.CFG", Encoding.ASCII.GetBytes("# FF.CFG - FlashFloppy configuration (served inside FAT by backend)\r\ninterface = ibmpc\r\nhost = acorn\r\nindex-suppression = no\r\n\r\n# Enable speaker / fake disk drive sounds\r\nsound = on\r\nsound-volume = 19\r\n\r\n# Disable display devices\r\ndisplay-type = none\r\n")); + img.AddFile("TEST.ADF", System.IO.File.ReadAllBytes("TestDisk.adf")); + + data = img.GetImage(); + img.SaveToFile("c:\\temp\\test_image1.img"); + + } + + + return new LoadImageResponse() { Success = true, Payload = new LoadImageResponsePayload() { DiskImage = data } }; + } + catch(Exception e) + { + return new LoadImageResponse() { Success = false, Error = GetExceptionPayload(e) }; + } + } + + private bool InRange(int lba, int offset, int len) + { + + long start = lba * DISK_SECTOR_SIZE + offset; + long end = start + len; + return end <= DISK_BYTE_SIZE; + } + + //[Authorize(Policy = "MustBeUser")] + [HttpPost("LoadSector")] + public async Task LoadSector(LoadSectorRequest request) + { + try + { + // Check image exists + + + // Check permissions of user + + + + // Get image data + byte[] data = null; + + if (System.IO.File.Exists("c:\\temp\\test_image1.img")) + { + + data = await System.IO.File.ReadAllBytesAsync("c:\\temp\\test_image1.img"); + } + else + { + var img = new Fat16Image(new Fat16ImageOptions + { + TotalSizeBytes = DISK_BYTE_SIZE, + VolumeLabel = "NetFloppy" + }); + + img.AddFile("README.TXT", Encoding.ASCII.GetBytes("Welcome to NetFloppy\r\n")); + img.AddFile("FF.CFG", Encoding.ASCII.GetBytes("# FF.CFG - FlashFloppy configuration (served inside FAT by backend)\r\ninterface = ibmpc\r\nhost = acorn\r\nindex-suppression = no\r\n\r\n# Enable speaker / fake disk drive sounds\r\nsound = on\r\nsound-volume = 19\r\n\r\n# Disable display devices\r\ndisplay-type = none\r\n")); + img.AddFile("TEST.ADF", System.IO.File.ReadAllBytes("TestDisk.adf")); + + data = img.GetImage(); + //img.SaveToFile("c:\\temp\\test_image1.img"); + + } + + if (!this.InRange(request.Payload.Lba, request.Payload.Offset, request.Payload.Length)) + { + throw new Exception("Invalid Disk Location"); + } + + var startPosition = (request.Payload.Lba * DISK_SECTOR_SIZE) + request.Payload.Offset; + var sectorData = data.Skip(startPosition).Take(request.Payload.Length).ToArray(); + + //Response.ContentType = "application/octet-stream"; + //Response.ContentLength = sectorData.Length; + //await Response.Body.WriteAsync(sectorData,0 , sectorData.Length); + + return new LoadSectorResponse() { Success = true, Payload = new LoadSectorResponsePayload() { Data = sectorData, Length = sectorData.Length } }; + + //return new EmptyResult(); + } + catch (Exception e) + { + //return new LoadSectorResponse() { Success = false, Error = GetExceptionPayload(e) }; + return BadRequest(e.Message); + } + } + + [Authorize(Policy = "MustBeUser")] + [HttpPost("SaveImage")] + public async Task SaveImage(SaveImageRequest request) + { + try + { + // Check image exists + + + // Check permissions of user + + + + // Get image data + var data = request.Payload.Data; + + // Save to file + await System.IO.File.WriteAllBytesAsync("c:\\temp\\test_image1.img", data); + + return new SaveImageResponse() { Success = true, Payload = new SaveImageResponsePayload() { BytesSaved = data.Length } }; + } + catch (Exception e) + { + return new SaveImageResponse() { Success = false, Error = GetExceptionPayload(e) }; + } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs new file mode 100644 index 0000000..d4c607b --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Controllers/RegisterController.cs @@ -0,0 +1,30 @@ +using AquaCubeIT.Service.NetFloppy.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace AquaCubeIT.Service.NetFloppy.Controllers; + +[ApiController] +[Route("[controller]")] +public class RegisterController : ControllerBase +{ + private readonly ILogger _logger; + + public RegisterController(ILogger logger) + { + _logger = logger; + } + + [Authorize(Policy = "MustBeUser")] + [HttpPost] + public RegisterResponse Post(RegisterRequest request) + { + return new RegisterResponse() { Success = true, Payload = new RegisterResponsePayload() { Name = "Test", Description = "Test Drive", EmulationType = "Unknown Floppy Type" }}; + } + + [HttpGet] + public RegisterResponse Get() + { + return new RegisterResponse() { Success = true, Payload = new RegisterResponsePayload() { Name = "Test", Description = "Test Drive", EmulationType = "Unknown Floppy Type" } }; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile new file mode 100644 index 0000000..a8132f0 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Dockerfile @@ -0,0 +1,30 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj", "AquaCubeIT.Service.NetFloppy/"] +RUN dotnet restore "./AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy.csproj" +COPY . . +WORKDIR "/src/AquaCubeIT.Service.NetFloppy" +RUN dotnet build "./AquaCubeIT.Service.NetFloppy.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./AquaCubeIT.Service.NetFloppy.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "AquaCubeIT.Service.NetFloppy.dll"] \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs new file mode 100644 index 0000000..2372a69 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat12Lib.cs @@ -0,0 +1,341 @@ +// Fat12ImageLib.cs +// Minimal C# library for creating a FAT12 disk image and adding files (root directory only) +// Target: .NET 6+ +// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Fat12Lib +{ + public class Fat12ImageOptions + { + // Total size of the image in bytes. Common sizes: 360 KiB, 720 KiB, 1.2 MiB, 1.44 MiB, 2.88 MiB, etc. + public long TotalSizeBytes { get; set; } = 1440L * 1024; // 1.44 MiB default + public ushort BytesPerSector { get; set; } = 512; + public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size + public byte NumFats { get; set; } = 2; + public ushort ReservedSectors { get; set; } = 1; // boot sector + public ushort MaxRootDirEntries { get; set; } = 224; // typical for 1.44MB floppy + public byte MediaDescriptor { get; set; } = 0xF0; // floppy + public ushort SectorsPerTrack { get; set; } = 18; // 1.44MB geometry (not critical) + public ushort NumberOfHeads { get; set; } = 2; // geometry (not critical) + public uint HiddenSectors { get; set; } = 0; + public string OemId { get; set; } = "MSDOS5.0"; + public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed + public string FsTypeLabel { get; set; } = "FAT12 "; // 8 chars in boot sector label + public ushort? VolumeId { get; set; } = null; // null => random + } + + public class Fat12Image + { + private readonly Fat12ImageOptions _opt; + + // Computed layout + private uint _totalSectors; // 16-bit or 32-bit depending on size + private ushort _rootDirSectors; + private ushort _fatSectors; // sectors per FAT + private uint _firstFatSectorLba; + private uint _firstRootDirSectorLba; + private uint _firstDataSectorLba; + private uint _clusterCount; // data clusters (not counting 0 and 1) + private byte _sectorsPerCluster; + + // Runtime buffers + private byte[] _bootSector = Array.Empty(); + private byte[] _fat; // a single FAT table; will be duplicated NumFats times + private byte[] _rootDir; // root directory area (fixed size) + private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps) + + // Allocation state + private uint _nextFreeCluster = 2; // FAT clusters start at 2 + private int _nextFreeRootDirEntry = 0; + + public Fat12Image(Fat12ImageOptions options) + { + _opt = options; + if (_opt.BytesPerSector != 512) + throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation."); + if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0) + throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector."); + + InitializeLayout(); + BuildBootSector(); + InitializeFatAndRoot(); + } + + // Adds a file into the root directory. Uses 8.3 name rules. + public void AddFile(string fileName, byte[] content, DateTime? timestamp = null) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName"); + if (content == null) throw new ArgumentNullException(nameof(content)); + + var (name83, ext83) = ToShortName(fileName); + if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries) + throw new IOException("Root directory is full."); + + // Compute how many clusters needed + uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster); + uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster); + if (clustersNeeded == 0) clustersNeeded = 1; // allocate 1 cluster to store 0-byte file metadata/simple handling + + if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount) + throw new IOException("Not enough free space for file."); + + // Allocate cluster chain + List chain = new(); + for (int i = 0; i < clustersNeeded; i++) + { + uint c = _nextFreeCluster++; + chain.Add(c); + } + + // Write data to clusters + int srcOffset = 0; + foreach (var c in chain) + { + uint lba = ClusterToLba(c); + uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector; + int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset)); + if (toCopy > 0) + Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy); + srcOffset += toCopy; + } + + // Link FAT entries (12-bit values) + for (int i = 0; i < chain.Count - 1; i++) + SetFat12Entry(chain[i], (ushort)chain[i + 1]); + SetFat12Entry(chain[^1], 0xFFF); // EOF for FAT12 + + // Build directory entry + var dt = timestamp ?? DateTime.Now; + ushort time = ToFatTime(dt); + ushort date = ToFatDate(dt); + + Span entry = stackalloc byte[32]; + entry.Clear(); + Encoding.ASCII.GetBytes(name83, entry); + Encoding.ASCII.GetBytes(ext83, entry.Slice(8)); + entry[11] = 0x20; // Archive + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx) + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster + BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size + + // Write entry into root dir + int rootOffset = _nextFreeRootDirEntry * 32; + _rootDir.AsSpan(rootOffset, 32).Clear(); + entry.CopyTo(_rootDir.AsSpan(rootOffset)); + _nextFreeRootDirEntry++; + } + + public byte[] GetImage() + { + using var ms = new MemoryStream((int)(_opt.TotalSizeBytes)); + + // Boot sector + ms.Write(_bootSector, 0, _bootSector.Length); + + // FATs + for (int f = 0; f < _opt.NumFats; f++) + ms.Write(_fat, 0, _fat.Length); + + // Root directory + ms.Write(_rootDir, 0, _rootDir.Length); + + // Data region + ms.Write(_data, 0, _data.Length); + + // Pad to exact size if necessary (should be exact) + if (ms.Length < _opt.TotalSizeBytes) + ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length)); + + return ms.ToArray(); + } + + public void SaveToFile(string path) + { + File.WriteAllBytes(path, GetImage()); + } + + private void InitializeLayout() + { + _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector); + + // Decide sectors per cluster if not specified + _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector); + + _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + + // Iteratively solve for FAT size and cluster count (FAT12 uses 12-bit entries => 1.5 bytes per entry) + ushort fatSectors = 1; + while (true) + { + uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors; + uint clusterCount = dataSectors / _sectorsPerCluster; + if (clusterCount < 1 || clusterCount > 4084) + throw new NotSupportedException($"Cluster count {clusterCount} out of FAT12 bounds (needs 1..4,084). Adjust size or sectors/cluster."); + + // FAT12 entries: (clusterCount + 2) entries, each 12 bits => total bytes = ceil(3 * entries / 2) + uint entries = clusterCount + 2; + uint fatBytes = (entries * 3 + 1) / 2; // ceil(1.5 * entries) + ushort requiredFatSectors = (ushort)((fatBytes + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + if (requiredFatSectors == fatSectors) + { + _fatSectors = fatSectors; + _clusterCount = clusterCount; + break; + } + fatSectors = requiredFatSectors; + } + + _firstFatSectorLba = _opt.ReservedSectors; + _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors); + _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors; + + // Buffers + _bootSector = new byte[_opt.BytesPerSector]; + _fat = new byte[_fatSectors * _opt.BytesPerSector]; + _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector]; + uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors; + _data = new byte[dataSectorsFinal * _opt.BytesPerSector]; + } + + private void BuildBootSector() + { + Span bs = _bootSector; + bs.Clear(); + + // Jump + OEM + bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short + Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8)); + + // BIOS Parameter Block (BPB) + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector); + bs[13] = _sectorsPerCluster; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors); + bs[16] = _opt.NumFats; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries); + ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16); + bs[21] = _opt.MediaDescriptor; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors); + + // Extended BPB for FAT12/FAT16 + ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId); + string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant(); + label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray()); + label = label.PadRight(11).Substring(0, 11); + Encoding.ASCII.GetBytes(label, bs.Slice(43, 11)); + string fstype = (_opt.FsTypeLabel ?? "FAT12").PadRight(8).Substring(0, 8); + Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8)); + + // Minimal bootstrap code area (ignored for data disks) + bs[_opt.BytesPerSector - 2] = 0x55; // signature + bs[_opt.BytesPerSector - 1] = 0xAA; + } + + private void InitializeFatAndRoot() + { + // FAT initial entries for FAT12 + // First three bytes: media descriptor and end markers -> usually F0 FF FF for 0xF0 media + Array.Clear(_fat, 0, _fat.Length); + _fat[0] = _opt.MediaDescriptor; // e.g., 0xF0 + _fat[1] = 0xFF; + _fat[2] = 0xFF; // cluster 1 reserved/end + } + + private uint ClusterToLba(uint cluster) + { + if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster; + } + + private void SetFat12Entry(uint cluster, ushort value) + { + if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + // value is 12-bit + value &= 0x0FFF; + int index = (int)(cluster + (cluster / 2)); // floor(1.5 * cluster) + if ((cluster & 1) == 0) + { + // even cluster: low 12 bits starting at index + _fat[index] = (byte)((_fat[index] & 0x00) | (value & 0xFF)); + _fat[index + 1] = (byte)((_fat[index + 1] & 0xF0) | ((value >> 8) & 0x0F)); + } + else + { + // odd cluster: high 12 bits overlapping starting at index + _fat[index] = (byte)((_fat[index] & 0x0F) | ((value << 4) & 0xF0)); + _fat[index + 1] = (byte)((_fat[index + 1] & 0x00) | ((value >> 4) & 0xFF)); + } + } + + private static (string name, string ext) ToShortName(string fileName) + { + // Extract base and extension, sanitize to 8.3 uppercase ASCII + string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE"; + string ext = Path.GetExtension(fileName); + if (ext.StartsWith('.')) ext = ext.Substring(1); + + string Sanitize(string s) => new string(s.ToUpperInvariant() + .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_') + .ToArray()); + + string n = Sanitize(name); + string e = Sanitize(ext); + if (n.Length > 8) n = n.Substring(0, 8); + if (e.Length > 3) e = e.Substring(0, 3); + n = n.PadRight(8); + e = e.PadRight(3); + return (n, e); + } + + private static ushort ToFatDate(DateTime dt) + { + int year = Math.Clamp(dt.Year - 1980, 0, 127); + int month = Math.Clamp(dt.Month, 1, 12); + int day = Math.Clamp(dt.Day, 1, 31); + return (ushort)((year << 9) | (month << 5) | day); + } + + private static ushort ToFatTime(DateTime dt) + { + int hour = Math.Clamp(dt.Hour, 0, 23); + int minute = Math.Clamp(dt.Minute, 0, 59); + int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution + return (ushort)((hour << 11) | (minute << 5) | second); + } + + private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector) + { + // Simple mapping aimed at floppy-like sizes with 512B sectors + long sizeKiB = (long)totalSectors * bytesPerSector / 1024; + if (sizeKiB <= 720) return 1; // 360K/720K + if (sizeKiB <= 1440) return 1; // 1.44MB + if (sizeKiB <= 2400) return 1; // 2.4MB + if (sizeKiB <= 2880) return 1; // 2.88MB + if (sizeKiB <= 4096) return 2; // up to 4MB + if (sizeKiB <= 8192) return 4; // up to 8MB + return 8; // larger volumes still FAT12 but uncommon + } + } +} + +// --------- Example usage (not part of the library) --------- +// using System.Text; +// var img = new Fat12Lib.Fat12Image(new Fat12Lib.Fat12ImageOptions { TotalSizeBytes = 1440L * 1024, VolumeLabel = "MY12DISK" }); +// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT12!")); +// img.SaveToFile("fat12.img"); diff --git a/AquaCubeIT.Service.NetFloppy/Fat16Disk.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat16Disk.cs similarity index 100% rename from AquaCubeIT.Service.NetFloppy/Fat16Disk.cs rename to AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Helpers/Fat16Disk.cs diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs new file mode 100644 index 0000000..e176835 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Error.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class Error + { + public long ErrorNo { get; set; } + public string Message { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs new file mode 100644 index 0000000..2f2eb9a --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/ErrorResponsePayload.cs @@ -0,0 +1,11 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class ErrorResponsePayload + { + public long ErrorNo { get; set; } + public string? Message { get; set; } = null; + public Exception? Exception { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs new file mode 100644 index 0000000..55fafeb --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadImageRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs new file mode 100644 index 0000000..795f782 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageRequestPayload.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadImageRequestPayload + { + public string UnitId { get; set; } = string.Empty; + public string DiskId { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs new file mode 100644 index 0000000..e924981 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadImageResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs new file mode 100644 index 0000000..fc07d90 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadImageResponsePayload.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadImageResponsePayload + { + public byte[] DiskImage { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs new file mode 100644 index 0000000..1b7399d --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadSectorRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs new file mode 100644 index 0000000..d57b63e --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorRequestPayload.cs @@ -0,0 +1,13 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadSectorRequestPayload + { + public string UnitId { get; set; } = string.Empty; + public string DiskId { get; set; } = string.Empty; + public int Lba { get; set; } = 0; + public int Offset { get; set; } = 0; + public int Length { get; set; } = 0; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs new file mode 100644 index 0000000..debab07 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadSectorResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs new file mode 100644 index 0000000..57a6e73 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LoadSectorResponsePayload.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LoadSectorResponsePayload + { + public byte[] Data { get; set; } + public int Length { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs new file mode 100644 index 0000000..11c9c8e --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LogonRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs new file mode 100644 index 0000000..5ddc465 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonRequestPayload.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LogonRequestPayload + { + public string UserName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs new file mode 100644 index 0000000..7f87ecf --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LogonResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs new file mode 100644 index 0000000..2d99219 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/LogonResponsePayload.cs @@ -0,0 +1,11 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class LogonResponsePayload + { + public required string Token { get; set; } + public required DateTime TokenExpiry { get; set; } + public string RefreshToken { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs new file mode 100644 index 0000000..ad65566 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RefreshRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs new file mode 100644 index 0000000..4bf65b0 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshRequestPayload.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RefreshRequestPayload + { + public string RefreshToken { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs new file mode 100644 index 0000000..458b3d6 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RefreshResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs new file mode 100644 index 0000000..7c929b9 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RefreshResponsePayload.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RefreshResponsePayload : LogonResponsePayload + { + public string RefreshToken { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs new file mode 100644 index 0000000..fdad9c0 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterReponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RegisterResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs new file mode 100644 index 0000000..879e8ab --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RegisterRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs new file mode 100644 index 0000000..bb51f55 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterRequestPayload.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RegisterRequestPayload + { + public string UnitId { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs new file mode 100644 index 0000000..4370ad0 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/RegisterResponsePayload.cs @@ -0,0 +1,12 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class RegisterResponsePayload + { + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public DateTime ExpiryDate { get; set; } = DateTime.MaxValue; + public string EmulationType { get; set; } = string.Empty; + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs new file mode 100644 index 0000000..c26bef4 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Repsonse.cs @@ -0,0 +1,12 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class Response + { + public bool Success { get; set; } + // public Type Type { get; set; } = typeof(T); + public T? Payload { get; set; } + public ErrorResponsePayload? Error { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs new file mode 100644 index 0000000..468c26c --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/Request.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class Request + { + public long Id { get; set; } + public T? Payload { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs new file mode 100644 index 0000000..1f3ce21 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequest.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class SaveImageRequest : Request + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs new file mode 100644 index 0000000..a021d36 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageRequestPayload.cs @@ -0,0 +1,10 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class SaveImageRequestPayload + { + public string UnitId { get; set; } = string.Empty; + public byte[]? Data { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs new file mode 100644 index 0000000..90517b0 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponse.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class SaveImageResponse : Response + { + + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs new file mode 100644 index 0000000..e172967 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Models/SaveImageResponsePayload.cs @@ -0,0 +1,9 @@ +using System.Numerics; + +namespace AquaCubeIT.Service.NetFloppy.Models +{ + public class SaveImageResponsePayload + { + public int BytesSaved { get; set; } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs new file mode 100644 index 0000000..58f3ce8 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Program.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.SerializerOptions.WriteIndented = true; + options.SerializerOptions.UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow; +}); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])) + }; +}); +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("MustBeAdmin", p => p.RequireRole("Admin")); + options.AddPolicy("MustBeUser", p => p.RequireRole("Admin", "User")); + // options.AddPolicy("CanReadOrders", p => p.RequireClaim("permission", "orders.read")); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + + +//app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json new file mode 100644 index 0000000..9a441a6 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5250" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7289;http://localhost:5250" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf new file mode 100644 index 0000000..344d7c1 Binary files /dev/null and b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/TestDisk.adf differ diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json new file mode 100644 index 0000000..a2ef3ed --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/appsettings.json @@ -0,0 +1,20 @@ +{ + "Kestrel": { + "Endpoints": { + "Http": { "Url": "http://0.0.0.0:5250" }, + "Https": { "Url": "https://0.0.0.0:5251" } + } + }, + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Debug" + } + }, + "AllowedHosts": "*", + "Jwt": { + "Key": "k&FS^docD5woeaDVczTZfmb44ijwm%W9AfE`Zf2h#%wjc", + "Issuer": "NetFloppy", + "Audience": "NetFloppy User" + } +} diff --git a/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json new file mode 100644 index 0000000..8ed1410 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/AquaCubeIT.Service.NetFloppy/libman.json @@ -0,0 +1,5 @@ +{ + "version": "3.0", + "defaultProvider": "cdnjs", + "libraries": [] +} \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl b/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl new file mode 100644 index 0000000..aa415aa Binary files /dev/null and b/AquaCubeIT.Service.NetFloppy/TEst/TEst.dbmdl differ diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj new file mode 100644 index 0000000..b6dd4e1 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj @@ -0,0 +1,63 @@ + + + + Debug + AnyCPU + TEst + 2.0 + 4.1 + {89a44dbb-da85-4401-8b94-ed76e238e3bb} + Microsoft.Data.Tools.Schema.Sql.Sql160DatabaseSchemaProvider + Database + + + TEst + TEst + 1033, CI + BySchemaAndSchemaType + True + v4.7.2 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user new file mode 100644 index 0000000..0b07de1 --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/TEst/TEst.sqlproj.user @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql b/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql new file mode 100644 index 0000000..dde0eff --- /dev/null +++ b/AquaCubeIT.Service.NetFloppy/TEst/Table1.sql @@ -0,0 +1,20 @@ +CREATE TABLE [dbo].[Table1] +( + [Id] INT NOT NULL, + [ledger_start_transaction_id] bigint GENERATED ALWAYS AS TRANSACTION_ID START HIDDEN NOT NULL, + [ledger_start_sequence_number] bigint GENERATED ALWAYS AS SEQUENCE_NUMBER START +) +WITH +( + LEDGER = ON + ( + APPEND_ONLY = ON, + LEDGER_VIEW = [dbo].[Table1_Ledger] + ( + transaction_id_column_name = [ledger_transaction_id], + sequence_number_column_name = [ledger_sequence_number], + operation_type_column_name = [ledger_operation_type], + operation_type_desc_column_name = [ledger_operation_type_desc] + ) + ) +) \ No newline at end of file diff --git a/AquaCubeIT.Service.NetFloppy/Dockerfile b/Old/Dockerfile similarity index 100% rename from AquaCubeIT.Service.NetFloppy/Dockerfile rename to Old/Dockerfile diff --git a/Old/Fat16Disk.cs b/Old/Fat16Disk.cs new file mode 100644 index 0000000..223a87b --- /dev/null +++ b/Old/Fat16Disk.cs @@ -0,0 +1,335 @@ +// Fat16ImageLib.cs +// Minimal C# library for creating a FAT16 disk image and adding files (root directory only) +// Target: .NET 6+ +// Limitations: 8.3 filenames, root directory only (no subdirectories), no long file names, no deletions/compaction. + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Fat16Lib +{ + public class Fat16ImageOptions + { + // Total size of the image in bytes. Common sizes: 16 MiB, 32 MiB, 64 MiB, 128 MiB, 256 MiB, etc. + public long TotalSizeBytes { get; set; } = 32L * 1024 * 1024; // 32 MiB default + public ushort BytesPerSector { get; set; } = 512; + public byte SectorsPerCluster { get; set; } = 0; // 0 = auto-select based on size + public byte NumFats { get; set; } = 2; + public ushort ReservedSectors { get; set; } = 1; // boot sector + public ushort MaxRootDirEntries { get; set; } = 512; // typical for FAT16 + public byte MediaDescriptor { get; set; } = 0xF8; // fixed disk + public ushort SectorsPerTrack { get; set; } = 32; // geometry hints (not critical) + public ushort NumberOfHeads { get; set; } = 64; // geometry hints (not critical) + public uint HiddenSectors { get; set; } = 0; + public string OemId { get; set; } = "MSDOS5.0"; + public string VolumeLabel { get; set; } = "NO NAME "; // exactly 11 chars; will be padded/trimmed + public string FsTypeLabel { get; set; } = "FAT16 "; // 8 chars in boot sector label + public ushort? VolumeId { get; set; } = null; // null => random + } + + public class Fat16Image + { + private readonly Fat16ImageOptions _opt; + + // Computed layout + private uint _totalSectors; // 16-bit or 32-bit depending on size + private ushort _rootDirSectors; + private ushort _fatSectors; // sectors per FAT + private uint _firstFatSectorLba; + private uint _firstRootDirSectorLba; + private uint _firstDataSectorLba; + private uint _clusterCount; // data clusters (not counting 0 and 1) + private byte _sectorsPerCluster; + + // Runtime buffers + private byte[] _bootSector = Array.Empty(); + private byte[] _fat; // a single FAT table; will be duplicated NumFats times + private byte[] _rootDir; // root directory area (fixed size) + private byte[] _data; // data region sized to contain all clusters (clusterCount * spc * bps) + + // Allocation state + private uint _nextFreeCluster = 2; // FAT clusters start at 2 + private int _nextFreeRootDirEntry = 0; + + public Fat16Image(Fat16ImageOptions options) + { + _opt = options; + if (_opt.BytesPerSector != 512) + throw new NotSupportedException("Only 512-byte sectors supported in this minimal implementation."); + if (_opt.TotalSizeBytes % _opt.BytesPerSector != 0) + throw new ArgumentException("TotalSizeBytes must be a multiple of BytesPerSector."); + + InitializeLayout(); + BuildBootSector(); + InitializeFatAndRoot(); + } + + // Adds a file into the root directory. Uses 8.3 name rules. + public void AddFile(string fileName, byte[] content, DateTime? timestamp = null) + { + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("fileName"); + if (content == null) throw new ArgumentNullException(nameof(content)); + + var (name83, ext83) = ToShortName(fileName); + if (_nextFreeRootDirEntry >= _opt.MaxRootDirEntries) + throw new IOException("Root directory is full."); + + // Compute how many clusters needed + uint bytesPerCluster = (uint)(_opt.BytesPerSector * _sectorsPerCluster); + uint clustersNeeded = (uint)((content.Length + bytesPerCluster - 1) / bytesPerCluster); + if (clustersNeeded == 0) clustersNeeded = 1; // zero-length file occupies 0 clusters in FAT16 spec? Practically some tools allow 0. We'll allocate 1 for simplicity. + + if ((_nextFreeCluster - 2) + clustersNeeded > _clusterCount) + throw new IOException("Not enough free space for file."); + + // Allocate cluster chain + List chain = new(); + for (int i = 0; i < clustersNeeded; i++) + { + uint c = _nextFreeCluster++; + chain.Add(c); + } + + // Write data to clusters + int srcOffset = 0; + foreach (var c in chain) + { + uint lba = ClusterToLba(c); + uint offset = (lba - _firstDataSectorLba) * _opt.BytesPerSector; + int toCopy = (int)Math.Min(bytesPerCluster, (uint)(content.Length - srcOffset)); + if (toCopy > 0) + Buffer.BlockCopy(content, srcOffset, _data, (int)offset, toCopy); + // Zero the remainder of the cluster (already zeroed by default construction) + srcOffset += toCopy; + } + + // Link FAT entries + for (int i = 0; i < chain.Count - 1; i++) + SetFatEntry(chain[i], (ushort)chain[i + 1]); + SetFatEntry(chain[^1], 0xFFFF); // EOF + + // Build directory entry + var dt = timestamp ?? DateTime.Now; + ushort time = ToFatTime(dt); + ushort date = ToFatDate(dt); + + Span entry = stackalloc byte[32]; + entry.Clear(); + Encoding.ASCII.GetBytes(name83, entry); + Encoding.ASCII.GetBytes(ext83, entry.Slice(8)); + entry[11] = 0x20; // Archive + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(14), time); // Create time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(16), date); // Create date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(18), date); // Last access date (approx) + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(22), time); // Write time + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(24), date); // Write date + BinaryPrimitives.WriteUInt16LittleEndian(entry.Slice(26), (ushort)chain[0]); // First cluster + BinaryPrimitives.WriteUInt32LittleEndian(entry.Slice(28), (uint)content.Length); // File size + + // Write entry into root dir + int rootOffset = _nextFreeRootDirEntry * 32; + _rootDir.AsSpan(rootOffset, 32).Clear(); + entry.CopyTo(_rootDir.AsSpan(rootOffset)); + _nextFreeRootDirEntry++; + } + + public byte[] GetImage() + { + using var ms = new MemoryStream((int)(_opt.TotalSizeBytes)); + + // Boot sector + ms.Write(_bootSector, 0, _bootSector.Length); + + // FATs + for (int f = 0; f < _opt.NumFats; f++) + ms.Write(_fat, 0, _fat.Length); + + // Root directory + ms.Write(_rootDir, 0, _rootDir.Length); + + // Data region + ms.Write(_data, 0, _data.Length); + + // Pad to exact size if necessary (should be exact) + if (ms.Length < _opt.TotalSizeBytes) + ms.Write(new byte[_opt.TotalSizeBytes - ms.Length], 0, (int)(_opt.TotalSizeBytes - ms.Length)); + + return ms.ToArray(); + } + + public void SaveToFile(string path) + { + File.WriteAllBytes(path, GetImage()); + } + + private void InitializeLayout() + { + _totalSectors = (uint)(_opt.TotalSizeBytes / _opt.BytesPerSector); + + // Decide sectors per cluster if not specified, per rough guidance for FAT16 + _sectorsPerCluster = _opt.SectorsPerCluster != 0 ? _opt.SectorsPerCluster : SelectSectorsPerCluster(_totalSectors, _opt.BytesPerSector); + + _rootDirSectors = (ushort)(((uint)_opt.MaxRootDirEntries * 32 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + + // We need to iteratively solve for FAT size and cluster count + ushort fatSectors = 1; + while (true) + { + uint dataSectors = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * fatSectors) - _rootDirSectors; + uint clusterCount = dataSectors / _sectorsPerCluster; + if (clusterCount < 4085 || clusterCount >= 65525) + throw new NotSupportedException($"Cluster count {clusterCount} out of FAT16 bounds (needs 4,085..65,524). Adjust size or sectors/cluster."); + + // FAT16 entry size 2 bytes, need (clusterCount + 2) entries + ushort requiredFatSectors = (ushort)(((clusterCount + 2) * 2 + (_opt.BytesPerSector - 1)) / _opt.BytesPerSector); + if (requiredFatSectors == fatSectors) + { + _fatSectors = fatSectors; + _clusterCount = clusterCount; + break; + } + fatSectors = requiredFatSectors; + } + + _firstFatSectorLba = _opt.ReservedSectors; + _firstRootDirSectorLba = _firstFatSectorLba + (uint)(_opt.NumFats * _fatSectors); + _firstDataSectorLba = _firstRootDirSectorLba + _rootDirSectors; + + // Buffers + _bootSector = new byte[_opt.BytesPerSector]; + _fat = new byte[_fatSectors * _opt.BytesPerSector]; + _rootDir = new byte[_rootDirSectors * _opt.BytesPerSector]; + uint dataSectorsFinal = _totalSectors - _opt.ReservedSectors - (uint)(_opt.NumFats * _fatSectors) - _rootDirSectors; + _data = new byte[dataSectorsFinal * _opt.BytesPerSector]; + } + + private void BuildBootSector() + { + Span bs = _bootSector; + bs.Clear(); + + // Jump + OEM + bs[0] = 0xEB; bs[1] = 0x3C; bs[2] = 0x90; // JMP short + Encoding.ASCII.GetBytes((_opt.OemId ?? "MSDOS5.0").PadRight(8).Substring(0,8), bs.Slice(3, 8)); + + // BIOS Parameter Block (BPB) + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(11), _opt.BytesPerSector); + bs[13] = _sectorsPerCluster; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(14), _opt.ReservedSectors); + bs[16] = _opt.NumFats; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(17), _opt.MaxRootDirEntries); + ushort totalSectors16 = _totalSectors <= 0xFFFF ? (ushort)_totalSectors : (ushort)0; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(19), totalSectors16); + bs[21] = _opt.MediaDescriptor; + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(22), _fatSectors); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(24), _opt.SectorsPerTrack); + BinaryPrimitives.WriteUInt16LittleEndian(bs.Slice(26), _opt.NumberOfHeads); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(28), _opt.HiddenSectors); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(32), _totalSectors); + + // Extended BPB for FAT16 + ushort volId = _opt.VolumeId ?? (ushort)Random.Shared.Next(1, 0xFFFF); + BinaryPrimitives.WriteUInt32LittleEndian(bs.Slice(39), volId); + string label = (_opt.VolumeLabel ?? "NO NAME").ToUpperInvariant(); + label = new string(label.Where(ch => ch >= 0x20 && ch <= 0x7E).ToArray()); + label = label.PadRight(11).Substring(0, 11); + Encoding.ASCII.GetBytes(label, bs.Slice(43, 11)); + string fstype = (_opt.FsTypeLabel ?? "FAT16").PadRight(8).Substring(0, 8); + Encoding.ASCII.GetBytes(fstype, bs.Slice(54, 8)); + + // Minimal bootstrap code area (ignored for data disks) + bs[_opt.BytesPerSector - 2] = 0x55; // signature + bs[_opt.BytesPerSector - 1] = 0xAA; + } + + private void InitializeFatAndRoot() + { + // FAT initial entries + // Entry 0: media descriptor in low byte, 0xFF in high byte + SetFatEntryRaw(0, (ushort)(_opt.MediaDescriptor | 0xFF00)); + // Entry 1: EOF + SetFatEntryRaw(1, 0xFFFF); + // Zero rest (already zero) + } + + private uint ClusterToLba(uint cluster) + { + if (cluster < 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + return _firstDataSectorLba + (cluster - 2) * _sectorsPerCluster; + } + + private void SetFatEntry(uint cluster, ushort value) + { + if (cluster >= _clusterCount + 2) throw new ArgumentOutOfRangeException(nameof(cluster)); + SetFatEntryRaw(cluster, value); + } + + private void SetFatEntryRaw(uint cluster, ushort value) + { + int index = (int)(cluster * 2); + BinaryPrimitives.WriteUInt16LittleEndian(_fat.AsSpan(index, 2), value); + } + + private static (string name, string ext) ToShortName(string fileName) + { + // Extract base and extension, sanitize to 8.3 uppercase ASCII + string name = Path.GetFileNameWithoutExtension(fileName) ?? "FILE"; + string ext = Path.GetExtension(fileName); + if (ext.StartsWith('.')) ext = ext.Substring(1); + + string Sanitize(string s) => new string(s.ToUpperInvariant() + .Select(c => (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "!#$%&'()-@^_`{}~".Contains(c) ? c : '_') + .ToArray()); + + string n = Sanitize(name); + string e = Sanitize(ext); + if (n.Length > 8) n = n.Substring(0, 8); + if (e.Length > 3) e = e.Substring(0, 3); + n = n.PadRight(8); + e = e.PadRight(3); + return (n, e); + } + + private static ushort ToFatDate(DateTime dt) + { + int year = Math.Clamp(dt.Year - 1980, 0, 127); + int month = Math.Clamp(dt.Month, 1, 12); + int day = Math.Clamp(dt.Day, 1, 31); + return (ushort)((year << 9) | (month << 5) | day); + } + + private static ushort ToFatTime(DateTime dt) + { + int hour = Math.Clamp(dt.Hour, 0, 23); + int minute = Math.Clamp(dt.Minute, 0, 59); + int second = Math.Clamp(dt.Second / 2, 0, 29); // 2-second resolution + return (ushort)((hour << 11) | (minute << 5) | second); + } + + private static byte SelectSectorsPerCluster(uint totalSectors, ushort bytesPerSector) + { + // Very rough mapping consistent with FAT16 recommendations for 512-byte sectors + // Values chosen to keep cluster count in FAT16 range and cluster size reasonable + long sizeMiB = (long)totalSectors * bytesPerSector / (1024 * 1024); + return sizeMiB switch + { + <= 16 => 2, // 1 KiB clusters + <= 32 => 4, // 2 KiB + <= 64 => 8, // 4 KiB + <= 128 => 16, // 8 KiB + <= 256 => 32, // 16 KiB + <= 512 => 64, // 32 KiB + _ => 64, // 32 KiB clusters for larger images in this minimal impl + }; + } + } +} + +// --------- Example usage (not part of the library) --------- +// var img = new Fat16Lib.Fat16Image(new Fat16Lib.Fat16ImageOptions { TotalSizeBytes = 32L * 1024 * 1024, VolumeLabel = "MYDISK" }); +// img.AddFile("HELLO.TXT", Encoding.ASCII.GetBytes("Hello FAT16!\r\n")); +// img.SaveToFile("fat16.img"); diff --git a/AquaCubeIT.Service.NetFloppy/Program.cs b/Old/Program.cs similarity index 100% rename from AquaCubeIT.Service.NetFloppy/Program.cs rename to Old/Program.cs diff --git a/AquaCubeIT.Service.NetFloppy/appsettings.json b/Old/appsettings.json similarity index 100% rename from AquaCubeIT.Service.NetFloppy/appsettings.json rename to Old/appsettings.json diff --git a/AquaCubeIT.Service.NetFloppy/docker-compose.yml b/Old/docker-compose.yml similarity index 100% rename from AquaCubeIT.Service.NetFloppy/docker-compose.yml rename to Old/docker-compose.yml diff --git a/AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs b/Old/fat_16_image_lib.cs similarity index 100% rename from AquaCubeIT.Service.NetFloppy/fat_16_image_lib.cs rename to Old/fat_16_image_lib.cs