From 2b9d699fcc9768faadeab8a618ddf5f5d05ae41e Mon Sep 17 00:00:00 2001 From: Carl Johnsen Date: Thu, 12 Mar 2026 14:05:39 +0100 Subject: [PATCH] Added a parser for FAT32 boot sector --- .../DiskImage/Filesystem/Fat32BootSector.cs | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 proprietary/DiskImage/Filesystem/Fat32BootSector.cs diff --git a/proprietary/DiskImage/Filesystem/Fat32BootSector.cs b/proprietary/DiskImage/Filesystem/Fat32BootSector.cs new file mode 100644 index 000000000..5829aac9e --- /dev/null +++ b/proprietary/DiskImage/Filesystem/Fat32BootSector.cs @@ -0,0 +1,230 @@ +// 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; +}