// Copyright (c) 2026 Duplicati Inc. All rights reserved. using System; using System.Buffers.Binary; using System.Collections.Generic; using Duplicati.Proprietary.DiskImage.Filesystem.Fat32; using NUnit.Framework; using NUnit.Framework.Legacy; using Assert = NUnit.Framework.Legacy.ClassicAssert; #nullable enable namespace Duplicati.UnitTest.DiskImage.Fat32; /// /// Unit tests for the FAT table reader. /// [TestFixture] [Category("DiskImageUnit")] public class Fat32TableTests { /// /// Creates a valid FAT32 boot sector with known values for testing. /// private static byte[] CreateValidBootSector() { var bootSector = new byte[512]; // Bytes per sector at offset 0x0B (512 = 0x0200) BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(0x0B, 2), 512); // Sectors per cluster at offset 0x0D (8 sectors) bootSector[0x0D] = 8; // Reserved sector count at offset 0x0E (32 sectors) BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(0x0E, 2), 32); // Number of FATs at offset 0x10 (2 FATs) bootSector[0x10] = 2; // Total sectors 32 at offset 0x20 (1,000,000 sectors = ~512MB) BinaryPrimitives.WriteUInt32LittleEndian(bootSector.AsSpan(0x20, 4), 1_000_000); // FAT size 32 at offset 0x24 (244 sectors per FAT) BinaryPrimitives.WriteUInt32LittleEndian(bootSector.AsSpan(0x24, 4), 244); // Root cluster at offset 0x2C (cluster 2) BinaryPrimitives.WriteUInt32LittleEndian(bootSector.AsSpan(0x2C, 4), 2); // FSInfo sector at offset 0x30 (sector 1) BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(0x30, 2), 1); // Filesystem type string at offset 0x52 (8 bytes: "FAT32 ") var fsType = System.Text.Encoding.ASCII.GetBytes("FAT32 "); fsType.CopyTo(bootSector, 0x52); // Boot sector signature at offset 510: bytes 0x55, 0xAA on disk (0xAA55 as little-endian uint16) BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(510, 2), 0xAA55); return bootSector; } /// /// Creates FAT data with specified entries. /// private static byte[] CreateFatData(params uint[] entries) { var fatData = new byte[entries.Length * 4]; for (int i = 0; i < entries.Length; i++) { BinaryPrimitives.WriteUInt32LittleEndian(fatData.AsSpan(i * 4, 4), entries[i]); } return fatData; } [Test] public void Test_Fat32Table_FreeCluster_NotAllocated() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 2 as free var fatData = CreateFatData(0, 0, Fat32Table.FREE_CLUSTER); var fatTable = new Fat32Table(bootSector, fatData); Assert.IsFalse(fatTable.IsClusterAllocated(2), "Free cluster should not be allocated"); } [Test] public void Test_Fat32Table_AllocatedCluster_IsAllocated() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 2 pointing to cluster 3 (allocated) var fatData = CreateFatData(0, 0, 3, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); Assert.IsTrue(fatTable.IsClusterAllocated(2), "Cluster with valid next-cluster value should be allocated"); Assert.IsTrue(fatTable.IsClusterAllocated(3), "Cluster with EOC should be allocated"); } [Test] public void Test_Fat32Table_EndOfChain_IsAllocated() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Test all EOC values (0x0FFFFFF8 - 0x0FFFFFFF) for (uint eocValue = Fat32Table.EOC_MIN; eocValue <= Fat32Table.EOC_MAX; eocValue++) { var fatData = CreateFatData(0, 0, eocValue); var fatTable = new Fat32Table(bootSector, fatData); Assert.IsTrue(fatTable.IsClusterAllocated(2), $"Cluster with EOC value 0x{eocValue:X8} should be allocated"); } } [Test] public void Test_Fat32Table_BadCluster_NotAllocated() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 2 as bad var fatData = CreateFatData(0, 0, Fat32Table.BAD_CLUSTER); var fatTable = new Fat32Table(bootSector, fatData); Assert.IsFalse(fatTable.IsClusterAllocated(2), "Bad cluster should not be allocated"); } [Test] public void Test_Fat32Table_GetClusterChain_SingleCluster() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 2 having EOC immediately var fatData = CreateFatData(0, 0, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var chain = fatTable.GetClusterChain(2); Assert.AreEqual(1, chain.Count, "Single cluster chain should have 1 entry"); Assert.AreEqual(2u, chain[0], "Chain should contain the start cluster"); } [Test] public void Test_Fat32Table_GetClusterChain_MultipleClusters() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with chain: 2 -> 3 -> 4 -> EOC var fatData = CreateFatData(0, 0, 3, 4, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var chain = fatTable.GetClusterChain(2); Assert.AreEqual(3, chain.Count, "Chain should have 3 entries"); Assert.AreEqual(2u, chain[0], "First entry should be cluster 2"); Assert.AreEqual(3u, chain[1], "Second entry should be cluster 3"); Assert.AreEqual(4u, chain[2], "Third entry should be cluster 4"); } [Test] public void Test_Fat32Table_GetClusterChain_CircularDetection() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with circular chain: 2 -> 3 -> 2 (circular) var fatData = CreateFatData(0, 0, 3, 2); var fatTable = new Fat32Table(bootSector, fatData); var ex = Assert.Throws(() => fatTable.GetClusterChain(2)); StringAssert.Contains("Circular", ex!.Message); } [Test] public void Test_Fat32Table_AllocationBitmap_MatchesFatEntries() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with mixed entries: // 0: reserved (typically EOC or 0) // 1: reserved (typically EOC or 0) // 2: free // 3: allocated (points to 4) // 4: EOC (end of chain) // 5: bad cluster var fatData = CreateFatData( Fat32Table.FREE_CLUSTER, // cluster 0 Fat32Table.FREE_CLUSTER, // cluster 1 Fat32Table.FREE_CLUSTER, // cluster 2 (free) 4, // cluster 3 (allocated, points to 4) Fat32Table.EOC_MIN, // cluster 4 (end of chain) Fat32Table.BAD_CLUSTER // cluster 5 (bad) ); var fatTable = new Fat32Table(bootSector, fatData); // Reserved clusters 0 and 1 - treated as free (not allocated) Assert.IsFalse(fatTable.IsClusterAllocated(0), "Cluster 0 should not be allocated"); Assert.IsFalse(fatTable.IsClusterAllocated(1), "Cluster 1 should not be allocated"); // Cluster 2 is free Assert.IsFalse(fatTable.IsClusterAllocated(2), "Cluster 2 (free) should not be allocated"); // Cluster 3 is allocated (points to next cluster) Assert.IsTrue(fatTable.IsClusterAllocated(3), "Cluster 3 (allocated) should be allocated"); // Cluster 4 is EOC Assert.IsTrue(fatTable.IsClusterAllocated(4), "Cluster 4 (EOC) should be allocated"); // Cluster 5 is bad Assert.IsFalse(fatTable.IsClusterAllocated(5), "Cluster 5 (bad) should not be allocated"); // Verify counts Assert.AreEqual(6u, fatTable.TotalClusters, "Total clusters should be 6"); Assert.AreEqual(2u, fatTable.AllocatedClusters, "Allocated clusters should be 2 (clusters 3 and 4)"); Assert.AreEqual(4u, fatTable.FreeClusters, "Free clusters should be 4 (clusters 0, 1, 2, and 5 is counted as free since it's bad)"); } [Test] public void Test_Fat32Table_EntryMask_IgnoresUpperBits() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 2 having upper 4 bits set (should be masked) // 0xF0000003 with mask 0x0FFFFFFF = 0x00000003 (points to cluster 3) uint entryWithUpperBits = 0xF0000003; var fatData = CreateFatData(0, 0, entryWithUpperBits, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var nextCluster = fatTable.GetNextCluster(2); Assert.AreEqual(3u, nextCluster, "Upper 4 bits should be masked, leaving value 3"); Assert.IsTrue(fatTable.IsClusterAllocated(2), "Cluster should be allocated even with upper bits set"); } [Test] public void Test_Fat32Table_GetNextCluster_ReturnsMaskedValue() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT where cluster 2 points to cluster 5 (0x00000005) var fatData = CreateFatData(0, 0, 5, Fat32Table.EOC_MIN, Fat32Table.EOC_MIN, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var nextCluster = fatTable.GetNextCluster(2); Assert.AreEqual(5u, nextCluster, "GetNextCluster should return the next cluster number"); } [Test] public void Test_Fat32Table_ClusterOutOfRange_Throws() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); var fatData = CreateFatData(0, 0, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); // Try to access cluster beyond the FAT size var ex = Assert.Throws(() => fatTable.IsClusterAllocated(100)); StringAssert.Contains("out of range", ex!.Message); } [Test] public void Test_Fat32Table_GetClusterChain_StartClusterOutOfRange_Throws() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); var fatData = CreateFatData(0, 0, Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var ex = Assert.Throws(() => fatTable.GetClusterChain(100)); StringAssert.Contains("out of range", ex!.Message); } [Test] public void Test_Fat32Table_GetClusterChain_InvalidClusterInChain_Throws() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT where cluster 2 points to cluster 3 which is free (invalid in a chain) var fatData = CreateFatData(0, 0, 3, Fat32Table.FREE_CLUSTER); var fatTable = new Fat32Table(bootSector, fatData); var ex = Assert.Throws(() => fatTable.GetClusterChain(2)); StringAssert.Contains("Invalid cluster", ex!.Message); } [Test] public void Test_Fat32Table_Constructor_NullFatData_Throws() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); var ex = Assert.Throws(() => new Fat32Table(bootSector, null!)); Assert.AreEqual("fatData", ex!.ParamName); } [Test] public void Test_Fat32Table_EmptyClusterChain() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create FAT with cluster 0 having EOC var fatData = CreateFatData(Fat32Table.EOC_MIN); var fatTable = new Fat32Table(bootSector, fatData); var chain = fatTable.GetClusterChain(0); Assert.AreEqual(1, chain.Count, "Single cluster chain should have 1 entry"); Assert.AreEqual(0u, chain[0], "Chain should contain cluster 0"); } [Test] public void Test_Fat32Table_LongClusterChain() { var bootSectorData = CreateValidBootSector(); var bootSector = new Fat32BootSector(bootSectorData); // Create a chain of 10 clusters: 2 -> 3 -> 4 -> ... -> 11 -> EOC var entries = new List { 0, 0 }; // clusters 0 and 1 for (uint i = 2; i <= 10; i++) { entries.Add(i + 1); // each cluster points to the next } entries.Add(Fat32Table.EOC_MIN); // cluster 11 is EOC var fatData = CreateFatData(entries.ToArray()); var fatTable = new Fat32Table(bootSector, fatData); var chain = fatTable.GetClusterChain(2); Assert.AreEqual(10, chain.Count, "Chain should have 10 entries"); for (int i = 0; i < 10; i++) { Assert.AreEqual((uint)(i + 2), chain[i], $"Chain entry {i} should be cluster {i + 2}"); } } }