// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System; using System.Buffers.Binary; using System.Text; namespace Duplicati.Proprietary.DiskImage.Filesystem; /// /// 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 (0x55AA). /// private const ushort BootSectorSignature = 0x55AA; /// /// 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); /// /// 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 (0x55AA at offset 510) 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 the filesystem type string at offset 0x52 contains "FAT32" var fsType = Encoding.ASCII.GetString(bootSectorData.Slice(FilesystemTypeOffset, FilesystemTypeLength)); if (!fsType.Contains("FAT32")) throw new ArgumentException($"Invalid filesystem type: expected '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; }