// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System; using System.Buffers.Binary; using System.Collections; using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Partition; namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32; /// /// Reads and provides access to the FAT32 File Allocation Table (FAT). /// This class reads the FAT from disk and provides methods to query cluster allocation state /// and follow cluster chains. /// internal class Fat32Table { /// /// Value indicating a free cluster. /// public const uint FREE_CLUSTER = 0x00000000; /// /// Value indicating a bad cluster. /// public const uint BAD_CLUSTER = 0x0FFFFFF7; /// /// Minimum value indicating end of cluster chain. /// public const uint EOC_MIN = 0x0FFFFFF8; /// /// Maximum value indicating end of cluster chain. /// public const uint EOC_MAX = 0x0FFFFFFF; /// /// Mask for extracting the 28-bit FAT entry value. /// public const uint ENTRY_MASK = 0x0FFFFFFF; /// /// The boot sector containing FAT geometry information. /// private readonly Fat32BootSector m_bootSector; /// /// The raw FAT data read from disk. /// private readonly byte[] m_fatData; /// /// Bitmap for fast lookup of allocated clusters. /// Index 0 corresponds to cluster 0 (reserved), index 2 corresponds to cluster 2 (first data cluster). /// private readonly BitArray m_allocationBitmap; /// /// Total number of clusters in the FAT (including reserved clusters 0 and 1). /// private readonly uint m_totalClusters; /// /// Initializes a new instance of the class by reading the FAT from disk. /// /// The partition containing the FAT32 filesystem. /// The parsed boot sector containing FAT geometry. /// Cancellation token. /// Thrown if partition or bootSector is null. public Fat32Table(IPartition partition, Fat32BootSector bootSector, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(partition); m_bootSector = bootSector; // Calculate FAT size in bytes var fatSizeInBytes = (long)bootSector.FatSize32 * bootSector.BytesPerSector; // Calculate total number of FAT entries m_totalClusters = (uint)(fatSizeInBytes / 4); // Read the entire FAT into memory m_fatData = new byte[fatSizeInBytes]; using var stream = partition.OpenReadAsync(cancellationToken).Result; stream.Seek(bootSector.FatStartOffset, SeekOrigin.Begin); stream.ReadExactly(m_fatData); // Build the allocation bitmap m_allocationBitmap = BuildAllocationBitmap(); } /// /// Initializes a new instance of the class from raw FAT data. /// This constructor is primarily used for testing. /// /// The parsed boot sector containing FAT geometry. /// The raw FAT data. public Fat32Table(Fat32BootSector bootSector, byte[] fatData) { ArgumentNullException.ThrowIfNull(fatData); m_bootSector = bootSector; m_fatData = fatData; m_totalClusters = (uint)(fatData.Length / 4); m_allocationBitmap = BuildAllocationBitmap(); } /// /// Gets the total number of clusters in the FAT. /// public uint TotalClusters => m_totalClusters; /// /// Gets the number of allocated (in-use) clusters. /// public uint AllocatedClusters { get { var count = 0; foreach (bool bit in m_allocationBitmap) { if (bit) count++; } return (uint)count; } } /// /// Gets the number of free clusters. /// public uint FreeClusters => m_totalClusters - AllocatedClusters; /// /// Checks if a cluster is allocated (in use). /// /// The cluster number to check. /// True if the cluster is allocated (in use, not free, not bad); otherwise, false. /// Thrown if cluster number is out of range. public bool IsClusterAllocated(uint clusterNumber) { if (clusterNumber >= m_totalClusters) throw new ArgumentOutOfRangeException(nameof(clusterNumber), $"Cluster number {clusterNumber} is out of range. Valid range is 0 to {m_totalClusters - 1}."); return m_allocationBitmap[(int)clusterNumber]; } /// /// Gets the next cluster in a cluster chain. /// /// The current cluster number. /// The next cluster number in the chain, or the end-of-chain marker if this is the last cluster. /// Thrown if cluster number is out of range. public uint GetNextCluster(uint clusterNumber) { if (clusterNumber >= m_totalClusters) throw new ArgumentOutOfRangeException(nameof(clusterNumber), $"Cluster number {clusterNumber} is out of range. Valid range is 0 to {m_totalClusters - 1}."); var entry = GetFatEntry(clusterNumber); // Return the masked entry value (only lower 28 bits are valid) return entry & ENTRY_MASK; } /// /// Gets the full cluster chain starting from the specified cluster. /// /// The starting cluster number. /// A list of cluster numbers in the chain, including the start cluster. /// Thrown if start cluster is out of range. /// Thrown if a circular chain is detected. public List GetClusterChain(uint startCluster) { if (startCluster >= m_totalClusters) throw new ArgumentOutOfRangeException(nameof(startCluster), $"Cluster number {startCluster} is out of range. Valid range is 0 to {m_totalClusters - 1}."); var chain = new List(); var visited = new HashSet(); var currentCluster = startCluster; while (true) { // Check for circular chain if (!visited.Add(currentCluster)) throw new InvalidOperationException($"Circular cluster chain detected at cluster {currentCluster}."); chain.Add(currentCluster); var nextCluster = GetNextCluster(currentCluster); // Check for end of chain if (IsEndOfChain(nextCluster)) break; // Check for bad or free cluster in the middle of a chain if (nextCluster == BAD_CLUSTER || nextCluster == FREE_CLUSTER) throw new InvalidOperationException($"Invalid cluster {nextCluster:X} found in chain at cluster {currentCluster}."); currentCluster = nextCluster; } return chain; } /// /// Gets the raw FAT entry value at the specified cluster index. /// /// The cluster number. /// The raw 32-bit FAT entry value. private uint GetFatEntry(uint clusterNumber) { var offset = (int)(clusterNumber * 4); return BinaryPrimitives.ReadUInt32LittleEndian(m_fatData.AsSpan(offset, 4)); } /// /// Builds the cluster allocation bitmap from the FAT data. /// /// A BitArray where each bit indicates whether the corresponding cluster is allocated. private BitArray BuildAllocationBitmap() { var bitmap = new BitArray((int)m_totalClusters); for (uint i = 0; i < m_totalClusters; i++) { var entry = GetFatEntry(i) & ENTRY_MASK; // A cluster is allocated if it's not free and not bad // EOC markers and valid next-cluster values all indicate allocation bool isAllocated = entry != FREE_CLUSTER && entry != BAD_CLUSTER; bitmap[(int)i] = isAllocated; } return bitmap; } /// /// Checks if a FAT entry value indicates end of cluster chain. /// /// The FAT entry value. /// True if the entry indicates end of chain; otherwise, false. private static bool IsEndOfChain(uint entry) { return entry >= EOC_MIN && entry <= EOC_MAX; } }