Added tests for the FAT32 table reader

This commit is contained in:
Carl Johnsen
2026-03-12 14:23:36 +01:00
parent 2ff2de5e83
commit bc01ef75ff
@@ -0,0 +1,351 @@
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using Duplicati.Proprietary.DiskImage.Filesystem;
using NUnit.Framework;
using NUnit.Framework.Legacy;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
#nullable enable
namespace Duplicati.UnitTest.DiskImage
{
/// <summary>
/// Unit tests for the <see cref="Fat32Table"/> FAT table reader.
/// </summary>
[TestFixture]
[Category("DiskImageUnit")]
public class Fat32TableTests
{
/// <summary>
/// Creates a valid FAT32 boot sector with known values for testing.
/// </summary>
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 (0x55AA)
BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(510, 2), 0x55AA);
return bootSector;
}
/// <summary>
/// Creates FAT data with specified entries.
/// </summary>
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<InvalidOperationException>(() => 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<ArgumentOutOfRangeException>(() => 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<ArgumentOutOfRangeException>(() => 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<InvalidOperationException>(() => 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<ArgumentNullException>(() => 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<uint> { 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}");
}
}
}
}