// 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");