mirror of
https://github.com/duplicati/duplicati.git
synced 2026-05-06 07:16:38 -04:00
Post restructure namespace fixes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,233 +3,232 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
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
|
||||
namespace Duplicati.UnitTest.DiskImage.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32BootSector"/> parser.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32BootSectorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32BootSector"/> parser.
|
||||
/// Creates a valid FAT32 boot sector with known values for testing.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32BootSectorTests
|
||||
private static byte[] CreateValidBootSector()
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a valid FAT32 boot sector with known values for testing.
|
||||
/// </summary>
|
||||
private static byte[] CreateValidBootSector()
|
||||
{
|
||||
var bootSector = new byte[512];
|
||||
var bootSector = new byte[512];
|
||||
|
||||
// Bytes per sector at offset 0x0B (512 = 0x0200)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(0x0B, 2), 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;
|
||||
// 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);
|
||||
// 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;
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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);
|
||||
// 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;
|
||||
}
|
||||
return bootSector;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32BootSector_ParseValidBootSector()
|
||||
[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();
|
||||
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);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bootSectorData.AsSpan(0x0B, 2), bytesPerSector);
|
||||
|
||||
var bootSector = new Fat32BootSector(bootSectorData);
|
||||
Assert.AreEqual(512, bootSector.BytesPerSector);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,171 +9,171 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
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
|
||||
namespace Duplicati.UnitTest.DiskImage.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Filesystem"/> class and related types.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32FilesystemTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Filesystem"/> class and related types.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32FilesystemTests
|
||||
[Test]
|
||||
public void Test_Fat32FilesystemMetadata_Properties()
|
||||
{
|
||||
[Test]
|
||||
public void Test_Fat32FilesystemMetadata_Properties()
|
||||
var metadata = new Fat32FilesystemMetadata
|
||||
{
|
||||
var metadata = new Fat32FilesystemMetadata
|
||||
{
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
Assert.AreEqual(1024 * 1024, metadata.BlockSize);
|
||||
Assert.AreEqual(4096, metadata.ClusterSize);
|
||||
Assert.AreEqual(1000, metadata.TotalClusters);
|
||||
Assert.AreEqual(500, metadata.AllocatedClusters);
|
||||
Assert.AreEqual(500, metadata.FreeClusters);
|
||||
}
|
||||
Assert.AreEqual(1024 * 1024, metadata.BlockSize);
|
||||
Assert.AreEqual(4096, metadata.ClusterSize);
|
||||
Assert.AreEqual(1000, metadata.TotalClusters);
|
||||
Assert.AreEqual(500, metadata.AllocatedClusters);
|
||||
Assert.AreEqual(500, metadata.FreeClusters);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32FilesystemMetadata_RecordEquality()
|
||||
[Test]
|
||||
public void Test_Fat32FilesystemMetadata_RecordEquality()
|
||||
{
|
||||
var metadata1 = new Fat32FilesystemMetadata
|
||||
{
|
||||
var metadata1 = new Fat32FilesystemMetadata
|
||||
{
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
var metadata2 = new Fat32FilesystemMetadata
|
||||
{
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
var metadata3 = new Fat32FilesystemMetadata
|
||||
{
|
||||
BlockSize = 512 * 1024, // Different
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
Assert.AreEqual(metadata1, metadata2);
|
||||
Assert.AreNotEqual(metadata1, metadata3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_DefaultValues()
|
||||
var metadata2 = new Fat32FilesystemMetadata
|
||||
{
|
||||
var file = new Fat32File();
|
||||
BlockSize = 1024 * 1024,
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
Assert.IsNull(file.Path);
|
||||
Assert.IsNull(file.Address);
|
||||
Assert.AreEqual(0, file.Size);
|
||||
Assert.IsFalse(file.IsDirectory);
|
||||
Assert.IsNull(file.LastModified);
|
||||
Assert.IsFalse(file.IsAllocated);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_WithValues()
|
||||
var metadata3 = new Fat32FilesystemMetadata
|
||||
{
|
||||
var timestamp = new DateTime(2023, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var file = new Fat32File
|
||||
{
|
||||
Path = "test_block",
|
||||
Address = 1024,
|
||||
Size = 1024 * 1024,
|
||||
LastModified = timestamp,
|
||||
IsAllocated = true
|
||||
};
|
||||
BlockSize = 512 * 1024, // Different
|
||||
ClusterSize = 4096,
|
||||
TotalClusters = 1000,
|
||||
AllocatedClusters = 500,
|
||||
FreeClusters = 500
|
||||
};
|
||||
|
||||
Assert.AreEqual("test_block", file.Path);
|
||||
Assert.AreEqual(1024, file.Address);
|
||||
Assert.AreEqual(1024 * 1024, file.Size);
|
||||
Assert.IsFalse(file.IsDirectory);
|
||||
Assert.AreEqual(timestamp, file.LastModified);
|
||||
Assert.IsTrue(file.IsAllocated);
|
||||
}
|
||||
Assert.AreEqual(metadata1, metadata2);
|
||||
Assert.AreNotEqual(metadata1, metadata3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_ImplementsIFile()
|
||||
[Test]
|
||||
public void Test_Fat32File_DefaultValues()
|
||||
{
|
||||
var file = new Fat32File();
|
||||
|
||||
Assert.IsNull(file.Path);
|
||||
Assert.IsNull(file.Address);
|
||||
Assert.AreEqual(0, file.Size);
|
||||
Assert.IsFalse(file.IsDirectory);
|
||||
Assert.IsNull(file.LastModified);
|
||||
Assert.IsFalse(file.IsAllocated);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_WithValues()
|
||||
{
|
||||
var timestamp = new DateTime(2023, 6, 15, 10, 30, 0, DateTimeKind.Utc);
|
||||
var file = new Fat32File
|
||||
{
|
||||
var file = new Fat32File();
|
||||
Assert.IsInstanceOf<IFile>(file);
|
||||
}
|
||||
Path = "test_block",
|
||||
Address = 1024,
|
||||
Size = 1024 * 1024,
|
||||
LastModified = timestamp,
|
||||
IsAllocated = true
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_LastModified_Nullable()
|
||||
{
|
||||
var file1 = new Fat32File { LastModified = DateTime.UtcNow };
|
||||
var file2 = new Fat32File { LastModified = null };
|
||||
Assert.AreEqual("test_block", file.Path);
|
||||
Assert.AreEqual(1024, file.Address);
|
||||
Assert.AreEqual(1024 * 1024, file.Size);
|
||||
Assert.IsFalse(file.IsDirectory);
|
||||
Assert.AreEqual(timestamp, file.LastModified);
|
||||
Assert.IsTrue(file.IsAllocated);
|
||||
}
|
||||
|
||||
Assert.IsTrue(file1.LastModified.HasValue);
|
||||
Assert.IsFalse(file2.LastModified.HasValue);
|
||||
}
|
||||
[Test]
|
||||
public void Test_Fat32File_ImplementsIFile()
|
||||
{
|
||||
var file = new Fat32File();
|
||||
Assert.IsInstanceOf<IFile>(file);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_IsAllocated_FalseByDefault()
|
||||
{
|
||||
var file = new Fat32File();
|
||||
Assert.IsFalse(file.IsAllocated);
|
||||
}
|
||||
[Test]
|
||||
public void Test_Fat32File_LastModified_Nullable()
|
||||
{
|
||||
var file1 = new Fat32File { LastModified = DateTime.UtcNow };
|
||||
var file2 = new Fat32File { LastModified = null };
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_IsAllocated_CanBeTrue()
|
||||
{
|
||||
var file = new Fat32File { IsAllocated = true };
|
||||
Assert.IsTrue(file.IsAllocated);
|
||||
}
|
||||
Assert.IsTrue(file1.LastModified.HasValue);
|
||||
Assert.IsFalse(file2.LastModified.HasValue);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_Address_LongNullable()
|
||||
{
|
||||
var file1 = new Fat32File { Address = 0 };
|
||||
var file2 = new Fat32File { Address = long.MaxValue };
|
||||
var file3 = new Fat32File { Address = null };
|
||||
[Test]
|
||||
public void Test_Fat32File_IsAllocated_FalseByDefault()
|
||||
{
|
||||
var file = new Fat32File();
|
||||
Assert.IsFalse(file.IsAllocated);
|
||||
}
|
||||
|
||||
Assert.AreEqual(0, file1.Address);
|
||||
Assert.AreEqual(long.MaxValue, file2.Address);
|
||||
Assert.IsNull(file3.Address);
|
||||
}
|
||||
[Test]
|
||||
public void Test_Fat32File_IsAllocated_CanBeTrue()
|
||||
{
|
||||
var file = new Fat32File { IsAllocated = true };
|
||||
Assert.IsTrue(file.IsAllocated);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_Size_Long()
|
||||
{
|
||||
var file1 = new Fat32File { Size = 0 };
|
||||
var file2 = new Fat32File { Size = 1024 * 1024 };
|
||||
var file3 = new Fat32File { Size = long.MaxValue };
|
||||
[Test]
|
||||
public void Test_Fat32File_Address_LongNullable()
|
||||
{
|
||||
var file1 = new Fat32File { Address = 0 };
|
||||
var file2 = new Fat32File { Address = long.MaxValue };
|
||||
var file3 = new Fat32File { Address = null };
|
||||
|
||||
Assert.AreEqual(0, file1.Size);
|
||||
Assert.AreEqual(1024 * 1024, file2.Size);
|
||||
Assert.AreEqual(long.MaxValue, file3.Size);
|
||||
}
|
||||
Assert.AreEqual(0, file1.Address);
|
||||
Assert.AreEqual(long.MaxValue, file2.Address);
|
||||
Assert.IsNull(file3.Address);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_Path_CanBeNull()
|
||||
{
|
||||
var file1 = new Fat32File { Path = null };
|
||||
var file2 = new Fat32File { Path = "test/path" };
|
||||
[Test]
|
||||
public void Test_Fat32File_Size_Long()
|
||||
{
|
||||
var file1 = new Fat32File { Size = 0 };
|
||||
var file2 = new Fat32File { Size = 1024 * 1024 };
|
||||
var file3 = new Fat32File { Size = long.MaxValue };
|
||||
|
||||
Assert.IsNull(file1.Path);
|
||||
Assert.AreEqual("test/path", file2.Path);
|
||||
}
|
||||
Assert.AreEqual(0, file1.Size);
|
||||
Assert.AreEqual(1024 * 1024, file2.Size);
|
||||
Assert.AreEqual(long.MaxValue, file3.Size);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32File_Path_CanBeNull()
|
||||
{
|
||||
var file1 = new Fat32File { Path = null };
|
||||
var file2 = new Fat32File { Path = "test/path" };
|
||||
|
||||
Assert.IsNull(file1.Path);
|
||||
Assert.AreEqual("test/path", file2.Path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,349 +3,348 @@
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
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
|
||||
namespace Duplicati.UnitTest.DiskImage.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Table"/> FAT table reader.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32TableTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Table"/> FAT table reader.
|
||||
/// Creates a valid FAT32 boot sector with known values for testing.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32TableTests
|
||||
private static byte[] CreateValidBootSector()
|
||||
{
|
||||
/// <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: bytes 0x55, 0xAA on disk (0xAA55 as little-endian uint16)
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bootSector.AsSpan(510, 2), 0xAA55);
|
||||
|
||||
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++)
|
||||
{
|
||||
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;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(fatData.AsSpan(i * 4, 4), entries[i]);
|
||||
}
|
||||
return fatData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates FAT data with specified entries.
|
||||
/// </summary>
|
||||
private static byte[] CreateFatData(params uint[] entries)
|
||||
[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 = 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 fatData = CreateFatData(0, 0, eocValue);
|
||||
var fatTable = new Fat32Table(bootSector, fatData);
|
||||
|
||||
Assert.IsFalse(fatTable.IsClusterAllocated(2), "Free cluster should not be allocated");
|
||||
Assert.IsTrue(fatTable.IsClusterAllocated(2), $"Cluster with EOC value 0x{eocValue:X8} should be allocated");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32Table_AllocatedCluster_IsAllocated()
|
||||
[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++)
|
||||
{
|
||||
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");
|
||||
entries.Add(i + 1); // each cluster points to the next
|
||||
}
|
||||
entries.Add(Fat32Table.EOC_MIN); // cluster 11 is EOC
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32Table_EndOfChain_IsAllocated()
|
||||
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++)
|
||||
{
|
||||
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}");
|
||||
}
|
||||
Assert.AreEqual((uint)(i + 2), chain[i], $"Chain entry {i} should be cluster {i + 2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,262 +3,261 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
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
|
||||
namespace Duplicati.UnitTest.DiskImage.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Filesystem.Fat32ZeroStream"/> class.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32ZeroStreamTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit tests for the <see cref="Fat32Filesystem.Fat32ZeroStream"/> class.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("DiskImageUnit")]
|
||||
public class Fat32ZeroStreamTests
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_ReturnsZeros()
|
||||
{
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_ReturnsZeros()
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");
|
||||
|
||||
// Verify all bytes are zero
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");
|
||||
|
||||
// Verify all bytes are zero
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test_Fat32ZeroStream_ReadAsync_ReturnsZeros()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");
|
||||
|
||||
// Verify all bytes are zero
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Length_MatchesBlockSize()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.AreEqual(blockSize, stream.Length, "Stream length should match block size");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_Works()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to beginning
|
||||
var position = stream.Seek(0, SeekOrigin.Begin);
|
||||
Assert.AreEqual(0, position, "Should be at position 0");
|
||||
|
||||
// Seek to middle
|
||||
position = stream.Seek(blockSize / 2, SeekOrigin.Begin);
|
||||
Assert.AreEqual(blockSize / 2, position, "Should be at middle position");
|
||||
|
||||
// Seek relative to current
|
||||
position = stream.Seek(1024, SeekOrigin.Current);
|
||||
Assert.AreEqual(blockSize / 2 + 1024, position, "Should be at relative position");
|
||||
|
||||
// Seek from end
|
||||
position = stream.Seek(-1024, SeekOrigin.End);
|
||||
Assert.AreEqual(blockSize - 1024, position, "Should be at position from end");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Write_ThrowsNotSupported()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<NotSupportedException>(() => stream.Write(buffer, 0, buffer.Length));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_MultipleInstances_ShareBuffer()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
|
||||
// Create two streams with the same block size
|
||||
using var stream1 = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
using var stream2 = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Both should read zeros
|
||||
var buffer1 = new byte[1024];
|
||||
var buffer2 = new byte[1024];
|
||||
|
||||
stream1.Read(buffer1, 0, buffer1.Length);
|
||||
stream2.Read(buffer2, 0, buffer2.Length);
|
||||
|
||||
// Verify both have zeros
|
||||
for (int i = 0; i < buffer1.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(0, buffer1[i], $"Buffer1 byte at position {i} should be zero");
|
||||
Assert.AreEqual(0, buffer2[i], $"Buffer2 byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_ReadPartial_ReturnsCorrectCount()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to near the end
|
||||
stream.Seek(blockSize - 100, SeekOrigin.Begin);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(100, bytesRead, "Should only read remaining bytes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_ReadPastEnd_ReturnsZero()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to end
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(0, bytesRead, "Should return 0 when reading past end");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_CanRead_CanWrite_CanSeek()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.IsTrue(stream.CanRead, "CanRead should be true");
|
||||
Assert.IsFalse(stream.CanWrite, "CanWrite should be false");
|
||||
Assert.IsTrue(stream.CanSeek, "CanSeek should be true");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_InvalidBlockSize_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(0));
|
||||
Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_SetLength_ThrowsNotSupported()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => stream.SetLength(1024));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Flush_DoesNotThrow()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.DoesNotThrow(() => stream.Flush());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Position_Property()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.AreEqual(0, stream.Position, "Initial position should be 0");
|
||||
|
||||
stream.Position = 1024;
|
||||
Assert.AreEqual(1024, stream.Position, "Position should be settable");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Position_InvalidValue_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = -1);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = blockSize + 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_InvalidOrigin_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => stream.Seek(0, (SeekOrigin)999));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_OutOfRange_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(blockSize + 1, SeekOrigin.Begin));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_NullBuffer_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
#pragma warning disable CS8600, CS8625
|
||||
Assert.Throws<ArgumentNullException>(() => stream.Read(null, 0, 1024));
|
||||
#pragma warning restore CS8600, CS8625
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_InvalidOffset_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buffer, -1, 1024));
|
||||
Assert.Throws<ArgumentException>(() => stream.Read(buffer, buffer.Length + 1, 1024));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_InvalidCount_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buffer, 0, -1));
|
||||
Assert.Throws<ArgumentException>(() => stream.Read(buffer, 512, 1024)); // Exceeds buffer length
|
||||
Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Test_Fat32ZeroStream_ReadAsync_ReturnsZeros()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(1024, bytesRead, "Should read requested number of bytes");
|
||||
|
||||
// Verify all bytes are zero
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(0, buffer[i], $"Byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Length_MatchesBlockSize()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.AreEqual(blockSize, stream.Length, "Stream length should match block size");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_Works()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to beginning
|
||||
var position = stream.Seek(0, SeekOrigin.Begin);
|
||||
Assert.AreEqual(0, position, "Should be at position 0");
|
||||
|
||||
// Seek to middle
|
||||
position = stream.Seek(blockSize / 2, SeekOrigin.Begin);
|
||||
Assert.AreEqual(blockSize / 2, position, "Should be at middle position");
|
||||
|
||||
// Seek relative to current
|
||||
position = stream.Seek(1024, SeekOrigin.Current);
|
||||
Assert.AreEqual(blockSize / 2 + 1024, position, "Should be at relative position");
|
||||
|
||||
// Seek from end
|
||||
position = stream.Seek(-1024, SeekOrigin.End);
|
||||
Assert.AreEqual(blockSize - 1024, position, "Should be at position from end");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Write_ThrowsNotSupported()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<NotSupportedException>(() => stream.Write(buffer, 0, buffer.Length));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_MultipleInstances_ShareBuffer()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
|
||||
// Create two streams with the same block size
|
||||
using var stream1 = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
using var stream2 = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Both should read zeros
|
||||
var buffer1 = new byte[1024];
|
||||
var buffer2 = new byte[1024];
|
||||
|
||||
stream1.Read(buffer1, 0, buffer1.Length);
|
||||
stream2.Read(buffer2, 0, buffer2.Length);
|
||||
|
||||
// Verify both have zeros
|
||||
for (int i = 0; i < buffer1.Length; i++)
|
||||
{
|
||||
Assert.AreEqual(0, buffer1[i], $"Buffer1 byte at position {i} should be zero");
|
||||
Assert.AreEqual(0, buffer2[i], $"Buffer2 byte at position {i} should be zero");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_ReadPartial_ReturnsCorrectCount()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to near the end
|
||||
stream.Seek(blockSize - 100, SeekOrigin.Begin);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(100, bytesRead, "Should only read remaining bytes");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_ReadPastEnd_ReturnsZero()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
// Seek to end
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
int bytesRead = stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
Assert.AreEqual(0, bytesRead, "Should return 0 when reading past end");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_CanRead_CanWrite_CanSeek()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.IsTrue(stream.CanRead, "CanRead should be true");
|
||||
Assert.IsFalse(stream.CanWrite, "CanWrite should be false");
|
||||
Assert.IsTrue(stream.CanSeek, "CanSeek should be true");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_InvalidBlockSize_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(0));
|
||||
Assert.Throws<ArgumentException>(() => new Fat32Filesystem.Fat32ZeroStream(-1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_SetLength_ThrowsNotSupported()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => stream.SetLength(1024));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Flush_DoesNotThrow()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.DoesNotThrow(() => stream.Flush());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Position_Property()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.AreEqual(0, stream.Position, "Initial position should be 0");
|
||||
|
||||
stream.Position = 1024;
|
||||
Assert.AreEqual(1024, stream.Position, "Position should be settable");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Position_InvalidValue_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = -1);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = blockSize + 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_InvalidOrigin_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => stream.Seek(0, (SeekOrigin)999));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Seek_OutOfRange_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(blockSize + 1, SeekOrigin.Begin));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_NullBuffer_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
#pragma warning disable CS8600, CS8625
|
||||
Assert.Throws<ArgumentNullException>(() => stream.Read(null, 0, 1024));
|
||||
#pragma warning restore CS8600, CS8625
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_InvalidOffset_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buffer, -1, 1024));
|
||||
Assert.Throws<ArgumentException>(() => stream.Read(buffer, buffer.Length + 1, 1024));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test_Fat32ZeroStream_Read_InvalidCount_Throws()
|
||||
{
|
||||
const int blockSize = 1024 * 1024; // 1MB
|
||||
using var stream = new Fat32Filesystem.Fat32ZeroStream(blockSize);
|
||||
|
||||
var buffer = new byte[1024];
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Read(buffer, 0, -1));
|
||||
Assert.Throws<ArgumentException>(() => stream.Read(buffer, 512, 1024)); // Exceeds buffer length
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,70 +24,69 @@ using System.Runtime.InteropServices;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Duplicati.UnitTest.DiskImage
|
||||
namespace Duplicati.UnitTest.DiskImage.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Factory class for creating the appropriate <see cref="IDiskImageHelper"/> implementation
|
||||
/// based on the host operating system.
|
||||
/// </summary>
|
||||
public static class DiskImageHelperFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory class for creating the appropriate <see cref="IDiskImageHelper"/> implementation
|
||||
/// based on the host operating system.
|
||||
/// Creates and returns the appropriate disk image helper for the current operating system.
|
||||
/// </summary>
|
||||
public static class DiskImageHelperFactory
|
||||
/// <returns>An <see cref="IDiskImageHelper"/> implementation for the current OS.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the current OS is not supported.</exception>
|
||||
public static IDiskImageHelper Create()
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates and returns the appropriate disk image helper for the current operating system.
|
||||
/// </summary>
|
||||
/// <returns>An <see cref="IDiskImageHelper"/> implementation for the current OS.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the current OS is not supported.</exception>
|
||||
public static IDiskImageHelper Create()
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new WindowsDiskImageHelper();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return new LinuxDiskImageHelper();
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return new MacOSDiskImageHelper();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException($"Disk image operations are not supported on this operating system: {RuntimeInformation.OSDescription}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether disk image operations are supported on the current platform.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if disk image operations are supported; otherwise, <c>false</c>.</value>
|
||||
public static bool IsSupported
|
||||
{
|
||||
get
|
||||
{
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the current platform.
|
||||
/// </summary>
|
||||
/// <value>The platform name ("Windows", "Linux", "macOS", or "Unknown").</value>
|
||||
public static string CurrentPlatformName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return new WindowsDiskImageHelper();
|
||||
}
|
||||
return "Windows";
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return new LinuxDiskImageHelper();
|
||||
}
|
||||
return "Linux";
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return new MacOSDiskImageHelper();
|
||||
}
|
||||
return "macOS";
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException($"Disk image operations are not supported on this operating system: {RuntimeInformation.OSDescription}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether disk image operations are supported on the current platform.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if disk image operations are supported; otherwise, <c>false</c>.</value>
|
||||
public static bool IsSupported
|
||||
{
|
||||
get
|
||||
{
|
||||
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
||||
|| RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the current platform.
|
||||
/// </summary>
|
||||
/// <value>The platform name ("Windows", "Linux", "macOS", or "Unknown").</value>
|
||||
public static string CurrentPlatformName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return "Windows";
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
return "Linux";
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
return "macOS";
|
||||
else
|
||||
return "Unknown";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,131 +29,129 @@ using Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Duplicati.UnitTest.DiskImage
|
||||
namespace Duplicati.UnitTest.DiskImage.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for disk image unit tests.
|
||||
/// Provides common operations for creating, initializing, and managing disk images.
|
||||
/// </summary>
|
||||
internal static class DiskImageTestHelpers
|
||||
{
|
||||
private const long MiB = 1024 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// Helper methods for disk image unit tests.
|
||||
/// Provides common operations for creating, initializing, and managing disk images.
|
||||
/// Creates a raw disk interface for the specified disk identifier based on the current platform.
|
||||
/// </summary>
|
||||
internal static class DiskImageTestHelpers
|
||||
/// <param name="diskIdentifier">The disk identifier (e.g., \\.\PhysicalDriveN on Windows, /dev/loopN on Linux).</param>
|
||||
/// <returns>An initialized IRawDisk instance for the specified platform.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the current OS is not supported.</exception>
|
||||
internal static IRawDisk CreateRawDiskForIdentifier(string diskIdentifier)
|
||||
{
|
||||
private const long MiB = 1024 * 1024;
|
||||
if (OperatingSystem.IsWindows())
|
||||
return new Proprietary.DiskImage.Disk.Windows(diskIdentifier);
|
||||
else if (OperatingSystem.IsLinux())
|
||||
return new Linux(diskIdentifier);
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
return new Mac(diskIdentifier);
|
||||
else
|
||||
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw disk interface for the specified disk identifier based on the current platform.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The disk identifier (e.g., \\.\PhysicalDriveN on Windows, /dev/loopN on Linux).</param>
|
||||
/// <returns>An initialized IRawDisk instance for the specified platform.</returns>
|
||||
/// <exception cref="PlatformNotSupportedException">Thrown when the current OS is not supported.</exception>
|
||||
internal static IRawDisk CreateRawDiskForIdentifier(string diskIdentifier)
|
||||
/// <summary>
|
||||
/// Creates a disk image file with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="diskHelper">The disk image helper.</param>
|
||||
/// <param name="imagePath">The path where the disk image will be created.</param>
|
||||
/// <param name="size">The size of the disk in bytes.</param>
|
||||
/// <param name="tableType">The partition table type (GPT, MBR, etc.).</param>
|
||||
/// <param name="partitions">Array of tuples specifying filesystem type and size for each partition.</param>
|
||||
/// <returns>The disk identifier for the created disk.</returns>
|
||||
internal static string CreateDiskWithPartitions(
|
||||
IDiskImageHelper diskHelper,
|
||||
string imagePath,
|
||||
long size,
|
||||
PartitionTableType tableType,
|
||||
(FileSystemType, long)[] partitions)
|
||||
{
|
||||
var diskIdentifier = diskHelper.CreateDisk(imagePath, size);
|
||||
diskHelper.InitializeDisk(diskIdentifier, tableType, partitions);
|
||||
diskHelper.FlushDisk(diskIdentifier);
|
||||
diskHelper.Unmount(diskIdentifier);
|
||||
return diskIdentifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a partition with well-known test data (repeating pattern of 0x00-0xFF).
|
||||
/// </summary>
|
||||
/// <param name="rawDisk">The raw disk interface.</param>
|
||||
/// <param name="partitionOffset">The starting offset of the partition in bytes.</param>
|
||||
/// <param name="partitionSize">The size of the partition in bytes.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
internal static async Task FillPartitionWithTestDataAsync(
|
||||
IRawDisk rawDisk,
|
||||
long partitionOffset,
|
||||
long partitionSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const int bufferSize = 64 * 1024; // 64KB chunks
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
long bytesWritten = 0;
|
||||
long currentOffset = partitionOffset;
|
||||
|
||||
while (bytesWritten < partitionSize)
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return new Duplicati.Proprietary.DiskImage.Disk.Windows(diskIdentifier);
|
||||
else if (OperatingSystem.IsLinux())
|
||||
return new Duplicati.Proprietary.DiskImage.Disk.Linux(diskIdentifier);
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
return new Duplicati.Proprietary.DiskImage.Disk.Mac(diskIdentifier);
|
||||
else
|
||||
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a disk image file with the specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="diskHelper">The disk image helper.</param>
|
||||
/// <param name="imagePath">The path where the disk image will be created.</param>
|
||||
/// <param name="size">The size of the disk in bytes.</param>
|
||||
/// <param name="tableType">The partition table type (GPT, MBR, etc.).</param>
|
||||
/// <param name="partitions">Array of tuples specifying filesystem type and size for each partition.</param>
|
||||
/// <returns>The disk identifier for the created disk.</returns>
|
||||
internal static string CreateDiskWithPartitions(
|
||||
IDiskImageHelper diskHelper,
|
||||
string imagePath,
|
||||
long size,
|
||||
PartitionTableType tableType,
|
||||
(FileSystemType, long)[] partitions)
|
||||
{
|
||||
var diskIdentifier = diskHelper.CreateDisk(imagePath, size);
|
||||
diskHelper.InitializeDisk(diskIdentifier, tableType, partitions);
|
||||
diskHelper.FlushDisk(diskIdentifier);
|
||||
diskHelper.Unmount(diskIdentifier);
|
||||
return diskIdentifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills a partition with well-known test data (repeating pattern of 0x00-0xFF).
|
||||
/// </summary>
|
||||
/// <param name="rawDisk">The raw disk interface.</param>
|
||||
/// <param name="partitionOffset">The starting offset of the partition in bytes.</param>
|
||||
/// <param name="partitionSize">The size of the partition in bytes.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
internal static async Task FillPartitionWithTestDataAsync(
|
||||
IRawDisk rawDisk,
|
||||
long partitionOffset,
|
||||
long partitionSize,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
const int bufferSize = 64 * 1024; // 64KB chunks
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
long bytesWritten = 0;
|
||||
long currentOffset = partitionOffset;
|
||||
|
||||
while (bytesWritten < partitionSize)
|
||||
// Fill buffer with well-known pattern (repeating 0x00-0xFF)
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Fill buffer with well-known pattern (repeating 0x00-0xFF)
|
||||
for (int i = 0; i < buffer.Length; i++)
|
||||
{
|
||||
buffer[i] = (byte)((bytesWritten + i) & 0xFF);
|
||||
}
|
||||
|
||||
var remaining = partitionSize - bytesWritten;
|
||||
var toWrite = (int)Math.Min(bufferSize, remaining);
|
||||
|
||||
await rawDisk.WriteBytesAsync(currentOffset, buffer.AsMemory(0, toWrite), cancellationToken);
|
||||
|
||||
bytesWritten += toWrite;
|
||||
currentOffset += toWrite;
|
||||
buffer[i] = (byte)((bytesWritten + i) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file extension for disk images based on the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The platform-specific disk image extension (vhdx, img, or dmg).</returns>
|
||||
internal static string GetPlatformDiskImageExtension()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return "vhdx";
|
||||
else if (OperatingSystem.IsLinux())
|
||||
return "img";
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
return "dmg";
|
||||
else
|
||||
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||
}
|
||||
var remaining = partitionSize - bytesWritten;
|
||||
var toWrite = (int)Math.Min(bufferSize, remaining);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file if it exists, catching any exceptions.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to delete.</param>
|
||||
internal static void SafeDeleteFile(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
await rawDisk.WriteBytesAsync(currentOffset, buffer.AsMemory(0, toWrite), cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Failed to delete file {filePath}: {ex.Message}");
|
||||
}
|
||||
bytesWritten += toWrite;
|
||||
currentOffset += toWrite;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file extension for disk images based on the current platform.
|
||||
/// </summary>
|
||||
/// <returns>The platform-specific disk image extension (vhdx, img, or dmg).</returns>
|
||||
internal static string GetPlatformDiskImageExtension()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
return "vhdx";
|
||||
else if (OperatingSystem.IsLinux())
|
||||
return "img";
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
return "dmg";
|
||||
else
|
||||
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a file if it exists, catching any exceptions.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the file to delete.</param>
|
||||
internal static void SafeDeleteFile(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
File.Delete(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Warning: Failed to delete file {filePath}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,89 +25,88 @@ using Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Duplicati.UnitTest.DiskImage
|
||||
namespace Duplicati.UnitTest.DiskImage.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for disk image helper operations that work across different operating systems.
|
||||
/// Provides methods for creating, attaching, formatting, and managing virtual disk images
|
||||
/// for testing disk image backup and restore operations.
|
||||
/// </summary>
|
||||
public interface IDiskImageHelper : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for disk image helper operations that work across different operating systems.
|
||||
/// Provides methods for creating, attaching, formatting, and managing virtual disk images
|
||||
/// for testing disk image backup and restore operations.
|
||||
/// Creates a virtual disk file, attaches it to the system, and returns the physical drive path.
|
||||
/// </summary>
|
||||
public interface IDiskImageHelper : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a virtual disk file, attaches it to the system, and returns the physical drive path.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path where the disk image file will be created.</param>
|
||||
/// <param name="sizeB">The size of the disk image in bytes.</param>
|
||||
/// <returns>The physical drive path (e.g., \\.\PhysicalDriveN on Windows, /dev/loopN on Linux).</returns>
|
||||
string CreateDisk(string imagePath, long sizeB);
|
||||
/// <param name="imagePath">The path where the disk image file will be created.</param>
|
||||
/// <param name="sizeB">The size of the disk image in bytes.</param>
|
||||
/// <returns>The physical drive path (e.g., \\.\PhysicalDriveN on Windows, /dev/loopN on Linux).</returns>
|
||||
string CreateDisk(string imagePath, long sizeB);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes, partitions, and formats a disk based on the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to initialize.</param>
|
||||
/// <param name="tableType">The type of partition table to create (e.g., GPT, MBR).</param>
|
||||
/// <param name="partitions">An array of tuples specifying the file system type and size for each partition. If the size is 0, the partition will use the remaining available space.</param>
|
||||
/// <returns>An array of mount points for the created partitions.</returns>
|
||||
string[] InitializeDisk(string diskIdentifier, PartitionTableType tableType, (FileSystemType, long)[] partitions);
|
||||
/// <summary>
|
||||
/// Initializes, partitions, and formats a disk based on the provided parameters
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to initialize.</param>
|
||||
/// <param name="tableType">The type of partition table to create (e.g., GPT, MBR).</param>
|
||||
/// <param name="partitions">An array of tuples specifying the file system type and size for each partition. If the size is 0, the partition will use the remaining available space.</param>
|
||||
/// <returns>An array of mount points for the created partitions.</returns>
|
||||
string[] InitializeDisk(string diskIdentifier, PartitionTableType tableType, (FileSystemType, long)[] partitions);
|
||||
|
||||
/// <summary>
|
||||
/// Mounts all of the partitions on the specified disk and returns their mount points.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to mount.</param>
|
||||
/// <param name="baseMountPath">Optional base path for mounting the partitions.</param>
|
||||
/// <param name="readOnly">Indicates whether the partitions should be mounted as read-only.</param>
|
||||
/// <param name="fileSystemTypes">Optional array of file system types for the partitions.</param>
|
||||
/// <returns>An array of mount points for the mounted partitions.</returns>
|
||||
string[] Mount(string diskIdentifier, string? baseMountPath = null, bool readOnly = false, FileSystemType[]? fileSystemTypes = null);
|
||||
/// <summary>
|
||||
/// Mounts all of the partitions on the specified disk and returns their mount points.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to mount.</param>
|
||||
/// <param name="baseMountPath">Optional base path for mounting the partitions.</param>
|
||||
/// <param name="readOnly">Indicates whether the partitions should be mounted as read-only.</param>
|
||||
/// <param name="fileSystemTypes">Optional array of file system types for the partitions.</param>
|
||||
/// <returns>An array of mount points for the mounted partitions.</returns>
|
||||
string[] Mount(string diskIdentifier, string? baseMountPath = null, bool readOnly = false, FileSystemType[]? fileSystemTypes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Unmounts all of the partitions on the specified disk, ensuring they are safely detached from the system. The disk will still be attached, but will be pulled offline. After this call, the disk device can be written to.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to unmount.</param>
|
||||
void Unmount(string diskIdentifier);
|
||||
/// <summary>
|
||||
/// Unmounts all of the partitions on the specified disk, ensuring they are safely detached from the system. The disk will still be attached, but will be pulled offline. After this call, the disk device can be written to.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to unmount.</param>
|
||||
void Unmount(string diskIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up and deletes a disk image file, ensuring it is detached first.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the disk image file.</param>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to clean up. If null, the method will attempt to determine the disk based on the image path.</param>
|
||||
void CleanupDisk(string imagePath, string? diskIdentifier = null);
|
||||
/// <summary>
|
||||
/// Cleans up and deletes a disk image file, ensuring it is detached first.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the disk image file.</param>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to clean up. If null, the method will attempt to determine the disk based on the image path.</param>
|
||||
void CleanupDisk(string imagePath, string? diskIdentifier = null);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current process has the necessary privileges to perform disk operations.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if running with sufficient privileges; otherwise, <c>false</c>.</returns>
|
||||
bool HasRequiredPrivileges();
|
||||
/// <summary>
|
||||
/// Checks if the current process has the necessary privileges to perform disk operations.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if running with sufficient privileges; otherwise, <c>false</c>.</returns>
|
||||
bool HasRequiredPrivileges();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the partition table information for a given disk identifier.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk.</param>
|
||||
/// <returns>The partition table geometry of the disk.</returns>
|
||||
PartitionTableGeometry GetPartitionTable(string diskIdentifier);
|
||||
/// <summary>
|
||||
/// Retrieves the partition table information for a given disk identifier.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk.</param>
|
||||
/// <returns>The partition table geometry of the disk.</returns>
|
||||
PartitionTableGeometry GetPartitionTable(string diskIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the partition information for a given disk identifier.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk.</param>
|
||||
/// <returns>An array of partition geometries for the disk.</returns>
|
||||
PartitionGeometry[] GetPartitions(string diskIdentifier);
|
||||
/// <summary>
|
||||
/// Retrieves the partition information for a given disk identifier.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk.</param>
|
||||
/// <returns>An array of partition geometries for the disk.</returns>
|
||||
PartitionGeometry[] GetPartitions(string diskIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the specified disk. Important prior to reading the disk raw.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to flush.</param>
|
||||
void FlushDisk(string diskIdentifier);
|
||||
/// <summary>
|
||||
/// Flushes the specified disk. Important prior to reading the disk raw.
|
||||
/// </summary>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to flush.</param>
|
||||
void FlushDisk(string diskIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Re-attaches the disk, optionally as read-only. This is useful for ensuring the disk is in a clean state after unmounting, and can also be used to force a read-only re-attachment for testing read-only scenarios.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the disk image file.</param>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to re-attach.</param>
|
||||
/// <param name="tableType">The type of partition table on the disk.</param>
|
||||
/// <param name="readOnly">Indicates whether the disk should be re-attached as read-only.</param>
|
||||
/// <returns>The identifier of the re-attached disk.</returns>
|
||||
string ReAttach(string imagePath, string diskIdentifier, PartitionTableType tableType, bool readOnly = false);
|
||||
}
|
||||
/// <summary>
|
||||
/// Re-attaches the disk, optionally as read-only. This is useful for ensuring the disk is in a clean state after unmounting, and can also be used to force a read-only re-attachment for testing read-only scenarios.
|
||||
/// </summary>
|
||||
/// <param name="imagePath">The path to the disk image file.</param>
|
||||
/// <param name="diskIdentifier">The identifier of the disk to re-attach.</param>
|
||||
/// <param name="tableType">The type of partition table on the disk.</param>
|
||||
/// <param name="readOnly">Indicates whether the disk should be re-attached as read-only.</param>
|
||||
/// <returns>The identifier of the re-attached disk.</returns>
|
||||
string ReAttach(string imagePath, string diskIdentifier, PartitionTableType tableType, bool readOnly = false);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ using Duplicati.Proprietary.DiskImage.Disk;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
using Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for FAT32 filesystems, storing cluster and block size information.
|
||||
|
||||
@@ -4,7 +4,7 @@ using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parsed FAT32 Boot Sector (BIOS Parameter Block).
|
||||
|
||||
@@ -8,7 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Walks the FAT32 directory tree to extract file metadata and build a cluster-to-timestamp map.
|
||||
|
||||
@@ -9,7 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
namespace Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
|
||||
/// <summary>
|
||||
/// Reads and provides access to the FAT32 File Allocation Table (FAT).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Partition;
|
||||
namespace Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
/// <summary>
|
||||
/// Provides CRC32 calculation utilities for partition table operations.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Disk;
|
||||
namespace Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
/// <summary>
|
||||
/// A disposable struct that rents a byte array from the shared ArrayPool and returns it when disposed.
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Disk;
|
||||
namespace Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
/// <summary>
|
||||
/// A read-only stream that wraps a pooled byte array and returns it to the pool when disposed.
|
||||
|
||||
@@ -7,7 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Proprietary.DiskImage.Disk;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
@@ -6,7 +6,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Proprietary.DiskImage.Disk;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
@@ -12,6 +12,7 @@ using Duplicati.Library.Logging;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Proprietary.DiskImage.Disk;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
using Duplicati.Proprietary.DiskImage.Partition;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Duplicati.Library.Interface;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.SourceItems;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Duplicati.Proprietary.DiskImage.Partition;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem;
|
||||
using Duplicati.Library.Utility;
|
||||
using Duplicati.Proprietary.DiskImage.General;
|
||||
using Duplicati.Proprietary.DiskImage.Filesystem.Fat32;
|
||||
|
||||
namespace Duplicati.Proprietary.DiskImage.SourceItems;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user