235 lines
8.7 KiB
C#
235 lines
8.7 KiB
C#
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
|
|
|
|
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the <see cref="Fat32BootSector"/> parser.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("DiskImageUnit")]
|
|
public class Fat32BootSectorTests
|
|
{
|
|
/// <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 = 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;
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_ParseValidBootSector()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
|
|
Assert.AreEqual(512, bootSector.BytesPerSector);
|
|
Assert.AreEqual(8, bootSector.SectorsPerCluster);
|
|
Assert.AreEqual(32, bootSector.ReservedSectorCount);
|
|
Assert.AreEqual(2, bootSector.NumberOfFats);
|
|
Assert.AreEqual(1_000_000u, bootSector.TotalSectors32);
|
|
Assert.AreEqual(244u, bootSector.FatSize32);
|
|
Assert.AreEqual(2u, bootSector.RootCluster);
|
|
Assert.AreEqual(1, bootSector.FsInfoSector);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_InvalidSignature_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
// Change the signature to an invalid value
|
|
BinaryPrimitives.WriteUInt16LittleEndian(bootSectorData.AsSpan(510, 2), 0x1234);
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(bootSectorData));
|
|
StringAssert.Contains("signature", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_InvalidBytesPerSector_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
// Set bytes per sector to 768 (not a power of 2)
|
|
BinaryPrimitives.WriteUInt16LittleEndian(bootSectorData.AsSpan(0x0B, 2), 768);
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(bootSectorData));
|
|
StringAssert.Contains("BytesPerSector", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_InvalidSectorsPerCluster_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
// Set sectors per cluster to 3 (not a power of 2)
|
|
bootSectorData[0x0D] = 3;
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(bootSectorData));
|
|
StringAssert.Contains("SectorsPerCluster", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_DerivedProperties_Correct()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
|
|
// ClusterSize = BytesPerSector * SectorsPerCluster = 512 * 8 = 4096
|
|
Assert.AreEqual(4096, bootSector.ClusterSize);
|
|
|
|
// FatStartOffset = ReservedSectorCount * BytesPerSector = 32 * 512 = 16384
|
|
Assert.AreEqual(16384, bootSector.FatStartOffset);
|
|
|
|
// DataStartOffset = (ReservedSectorCount + NumberOfFats * FatSize32) * BytesPerSector
|
|
// = (32 + 2 * 244) * 512 = (32 + 488) * 512 = 520 * 512 = 266240
|
|
Assert.AreEqual(266240, bootSector.DataStartOffset);
|
|
|
|
// TotalDataClusters = (TotalSectors32 - (ReservedSectorCount + NumberOfFats * FatSize32)) / SectorsPerCluster
|
|
// = (1,000,000 - (32 + 2 * 244)) / 8 = (1,000,000 - 520) / 8 = 999,480 / 8 = 124935
|
|
Assert.AreEqual(124935u, bootSector.TotalDataClusters);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_ClusterToByteOffset_Correct()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
|
|
// Cluster 2 should be at DataStartOffset (first data cluster)
|
|
Assert.AreEqual(bootSector.DataStartOffset, bootSector.ClusterToByteOffset(2));
|
|
|
|
// Cluster 3 should be at DataStartOffset + ClusterSize
|
|
Assert.AreEqual(bootSector.DataStartOffset + bootSector.ClusterSize, bootSector.ClusterToByteOffset(3));
|
|
|
|
// Cluster 10 should be at DataStartOffset + 8 * ClusterSize
|
|
Assert.AreEqual(bootSector.DataStartOffset + 8 * bootSector.ClusterSize, bootSector.ClusterToByteOffset(10));
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_TooSmallBuffer_Throws()
|
|
{
|
|
var smallBuffer = new byte[256];
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(smallBuffer));
|
|
StringAssert.Contains("512", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_ClusterToByteOffset_InvalidCluster_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => bootSector.ClusterToByteOffset(0));
|
|
StringAssert.Contains("Cluster numbers start at 2", ex!.Message);
|
|
|
|
ex = Assert.Throws<ArgumentException>(() => bootSector.ClusterToByteOffset(1));
|
|
StringAssert.Contains("Cluster numbers start at 2", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_NumberOfFats_Zero_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
bootSectorData[0x10] = 0; // Set NumberOfFats to 0
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(bootSectorData));
|
|
StringAssert.Contains("NumberOfFats", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_InvalidFilesystemType_Throws()
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
// Change filesystem type to something other than FAT32
|
|
var fsType = Encoding.ASCII.GetBytes("FAT16 ");
|
|
fsType.CopyTo(bootSectorData, 0x52);
|
|
|
|
var ex = Assert.Throws<ArgumentException>(() => new Fat32BootSector(bootSectorData));
|
|
StringAssert.Contains("FAT32", ex!.Message);
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_ValidBytesPerSectorValues()
|
|
{
|
|
ushort[] validValues = { 512, 1024, 2048, 4096 };
|
|
|
|
foreach (var bytesPerSector in validValues)
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
BinaryPrimitives.WriteUInt16LittleEndian(bootSectorData.AsSpan(0x0B, 2), bytesPerSector);
|
|
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
Assert.AreEqual(bytesPerSector, bootSector.BytesPerSector);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_ValidSectorsPerClusterValues()
|
|
{
|
|
byte[] validValues = { 1, 2, 4, 8, 16, 32, 64, 128 };
|
|
|
|
foreach (var sectorsPerCluster in validValues)
|
|
{
|
|
var bootSectorData = CreateValidBootSector();
|
|
bootSectorData[0x0D] = sectorsPerCluster;
|
|
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
Assert.AreEqual(sectorsPerCluster, bootSector.SectorsPerCluster);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void Test_Fat32BootSector_LargerBuffer_Works()
|
|
{
|
|
// Test that a buffer larger than 512 bytes works (e.g., 4KB sector size)
|
|
var bootSectorData = new byte[4096];
|
|
var validData = CreateValidBootSector();
|
|
validData.CopyTo(bootSectorData, 0);
|
|
|
|
var bootSector = new Fat32BootSector(bootSectorData);
|
|
Assert.AreEqual(512, bootSector.BytesPerSector);
|
|
}
|
|
}
|