// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System; using System.Buffers.Binary; using System.Text; namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32; /// /// Represents a parsed FAT32 Boot Sector (BIOS Parameter Block). /// This is a read-only struct that extracts geometry information from the boot sector. /// public readonly record struct Fat32BootSector { /// /// The boot sector signature at offset 510. /// On disk the bytes are 0x55, 0xAA; read as little-endian uint16 this is 0xAA55. /// private const ushort BootSectorSignature = 0xAA55; /// /// Offset to the boot sector signature. /// private const int SignatureOffset = 510; /// /// Offset to the filesystem type string (should contain "FAT32"). /// private const int FilesystemTypeOffset = 0x52; /// /// Length of the filesystem type string (8 bytes). /// private const int FilesystemTypeLength = 8; /// /// Offset to BytesPerSector field. /// private const int BytesPerSectorOffset = 0x0B; /// /// Offset to SectorsPerCluster field. /// private const int SectorsPerClusterOffset = 0x0D; /// /// Offset to ReservedSectorCount field. /// private const int ReservedSectorCountOffset = 0x0E; /// /// Offset to NumberOfFats field. /// private const int NumberOfFatsOffset = 0x10; /// /// Offset to TotalSectors32 field. /// private const int TotalSectors32Offset = 0x20; /// /// Offset to FatSize32 field (sectors per FAT). /// private const int FatSize32Offset = 0x24; /// /// Offset to RootCluster field. /// private const int RootClusterOffset = 0x2C; /// /// Offset to FsInfoSector field. /// private const int FsInfoSectorOffset = 0x30; /// /// Minimum valid bytes per sector (512). /// private const ushort MinBytesPerSector = 512; /// /// Maximum valid bytes per sector (4096). /// private const ushort MaxBytesPerSector = 4096; /// /// Minimum valid sectors per cluster (1). /// private const byte MinSectorsPerCluster = 1; /// /// Maximum valid sectors per cluster (128). /// private const byte MaxSectorsPerCluster = 128; /// /// Minimum required buffer size (512 bytes for boot sector). /// private const int MinimumBufferSize = 512; /// /// Gets the number of bytes per sector (typically 512, 1024, 2048, or 4096). /// public ushort BytesPerSector { get; } /// /// Gets the number of sectors per cluster (power of 2: 1, 2, 4, 8, 16, 32, 64, 128). /// public byte SectorsPerCluster { get; } /// /// Gets the number of reserved sectors (includes boot sector and FSInfo). /// public ushort ReservedSectorCount { get; } /// /// Gets the number of FAT copies (typically 2). /// public byte NumberOfFats { get; } /// /// Gets the total number of sectors in the volume (32-bit value). /// public uint TotalSectors32 { get; } /// /// Gets the number of sectors per FAT. /// public uint FatSize32 { get; } /// /// Gets the root directory cluster number (typically 2). /// public uint RootCluster { get; } /// /// Gets the FSInfo sector number. /// public ushort FsInfoSector { get; } /// /// Gets the size of a cluster in bytes. /// public int ClusterSize => BytesPerSector * SectorsPerCluster; /// /// Gets the byte offset from the partition start to the first FAT. /// public long FatStartOffset => (long)ReservedSectorCount * BytesPerSector; /// /// Gets the byte offset from the partition start to the data region. /// public long DataStartOffset => (long)(ReservedSectorCount + NumberOfFats * FatSize32) * BytesPerSector; /// /// Gets the total number of data clusters in the volume. /// public uint TotalDataClusters => (uint)((TotalSectors32 - (ReservedSectorCount + NumberOfFats * FatSize32)) / SectorsPerCluster); /// /// Minimum valid total sectors for a FAT32 volume (1 MB = 2048 sectors at 512 bytes). /// private const uint MinTotalSectors = 2048; /// /// Minimum valid FAT size in sectors (for a small volume). /// private const uint MinFatSize = 1; /// /// Maximum valid FAT size in sectors (for very large volumes, e.g., 2TB with 4KB sectors). /// private const uint MaxFatSize = 16777215; /// /// Initializes a new instance of the struct by parsing the provided boot sector data. /// /// The boot sector data (must be at least 512 bytes). /// Thrown when the buffer is too small or validation fails. public Fat32BootSector(ReadOnlySpan bootSectorData) { if (bootSectorData.Length < MinimumBufferSize) throw new ArgumentException($"Boot sector data must be at least {MinimumBufferSize} bytes.", nameof(bootSectorData)); // Validate boot sector signature (bytes 0x55, 0xAA at offset 510; 0xAA55 as little-endian uint16) var signature = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(SignatureOffset, 2)); if (signature != BootSectorSignature) throw new ArgumentException($"Invalid boot sector signature: expected 0x{BootSectorSignature:X4}, got 0x{signature:X4}.", nameof(bootSectorData)); // Parse fields from the boot sector BytesPerSector = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(BytesPerSectorOffset, 2)); SectorsPerCluster = bootSectorData[SectorsPerClusterOffset]; ReservedSectorCount = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(ReservedSectorCountOffset, 2)); NumberOfFats = bootSectorData[NumberOfFatsOffset]; TotalSectors32 = BinaryPrimitives.ReadUInt32LittleEndian(bootSectorData.Slice(TotalSectors32Offset, 4)); FatSize32 = BinaryPrimitives.ReadUInt32LittleEndian(bootSectorData.Slice(FatSize32Offset, 4)); RootCluster = BinaryPrimitives.ReadUInt32LittleEndian(bootSectorData.Slice(RootClusterOffset, 4)); FsInfoSector = BinaryPrimitives.ReadUInt16LittleEndian(bootSectorData.Slice(FsInfoSectorOffset, 2)); // Validate BytesPerSector is a power of 2 (512, 1024, 2048, or 4096) if (!IsPowerOfTwo(BytesPerSector) || BytesPerSector < MinBytesPerSector || BytesPerSector > MaxBytesPerSector) throw new ArgumentException($"Invalid BytesPerSector: {BytesPerSector}. Must be a power of 2 between {MinBytesPerSector} and {MaxBytesPerSector}.", nameof(bootSectorData)); // Validate SectorsPerCluster is a power of 2 (1, 2, 4, 8, 16, 32, 64, or 128) if (!IsPowerOfTwo(SectorsPerCluster) || SectorsPerCluster < MinSectorsPerCluster || SectorsPerCluster > MaxSectorsPerCluster) throw new ArgumentException($"Invalid SectorsPerCluster: {SectorsPerCluster}. Must be a power of 2 between {MinSectorsPerCluster} and {MaxSectorsPerCluster}.", nameof(bootSectorData)); // Validate NumberOfFats >= 1 if (NumberOfFats < 1) throw new ArgumentException($"Invalid NumberOfFats: {NumberOfFats}. Must be at least 1.", nameof(bootSectorData)); // Validate TotalSectors32 is reasonable (at least 1 MB worth of sectors) if (TotalSectors32 < MinTotalSectors) throw new ArgumentException($"Invalid TotalSectors32: {TotalSectors32}. Must be at least {MinTotalSectors} (1 MB).", nameof(bootSectorData)); // Validate FatSize32 is reasonable if (FatSize32 < MinFatSize || FatSize32 > MaxFatSize) throw new ArgumentException($"Invalid FatSize32: {FatSize32}. Must be between {MinFatSize} and {MaxFatSize}.", nameof(bootSectorData)); // Validate RootCluster (typically 2, but allow any reasonable value >= 2) if (RootCluster < 2) throw new ArgumentException($"Invalid RootCluster: {RootCluster}. Must be at least 2.", nameof(bootSectorData)); // Validate ReservedSectorCount is reasonable (typically 0-65535) if (ReservedSectorCount < 1 || ReservedSectorCount > 65535) throw new ArgumentException($"Invalid ReservedSectorCount: {ReservedSectorCount}. Must be between 1 and 65535.", nameof(bootSectorData)); // Validate the filesystem type string at offset 0x52 contains "FAT32" (exact match) var fsType = Encoding.ASCII.GetString(bootSectorData.Slice(FilesystemTypeOffset, FilesystemTypeLength)); if (fsType != "FAT32 " && fsType != "FAT32") throw new ArgumentException($"Invalid filesystem type: expected 'FAT32 ' or 'FAT32' in filesystem type string, got '{fsType}'.", nameof(bootSectorData)); } /// /// Converts a cluster number to its byte offset in the data region. /// /// The cluster number (clusters are 2-indexed). /// The byte offset from the partition start. /// Thrown when cluster number is less than 2. public long ClusterToByteOffset(uint clusterNumber) { if (clusterNumber < 2) throw new ArgumentException("Cluster numbers start at 2.", nameof(clusterNumber)); return DataStartOffset + (clusterNumber - 2) * ClusterSize; } /// /// Checks if a value is a power of 2. /// /// The value to check. /// True if the value is a power of 2; otherwise, false. private static bool IsPowerOfTwo(ushort value) => value != 0 && (value & (value - 1)) == 0; /// /// Checks if a value is a power of 2. /// /// The value to check. /// True if the value is a power of 2; otherwise, false. private static bool IsPowerOfTwo(byte value) => value != 0 && (value & (value - 1)) == 0; }