using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Disk; using Duplicati.Proprietary.DiskImage.Filesystem; using Duplicati.Proprietary.DiskImage.General; using Duplicati.Proprietary.DiskImage.Partition; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; namespace Duplicati.UnitTest.DiskImage.UnitTests; #nullable enable public partial class DiskImageUnitTests : BasicSetupHelper { /// /// Tests that ListFilesAsync returns the expected block-based files for an UnknownFilesystem. /// [Test] public async Task Test_UnknownFilesystem_ListFilesAsync_ReturnsBlockBasedFiles_Async() { // Create a partition on the writable disk var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 10 * MiB; var partitionOffset = sectorSize * 2048; // Start at sector 2048 // Create a simple partition entry var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; // Create UnknownFilesystem with 1MB block size using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // List files var files = new List(); await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) files.Add(file); // Should have 10 files (10MB partition / 1MB block size) Assert.AreEqual(10, files.Count, "Should have 10 block files for a 10MB partition with 1MB blocks."); // Verify each file has correct properties for (int i = 0; i < files.Count; i++) { var file = files[i]; Assert.IsInstanceOf(file, $"File {i} should be UnknownFilesystemFile."); Assert.IsFalse(file.IsDirectory, $"File {i} should not be a directory."); var unknownFile = (UnknownFilesystemFile)file; Assert.AreEqual(i * 1024L * 1024L, unknownFile.Address, $"File {i} should have correct address."); // All files except possibly the last should have full block size if (i < files.Count - 1) Assert.AreEqual(1024 * 1024, unknownFile.Size, $"File {i} should have full block size."); } } /// /// Tests that write and read round-trip preserves data correctly. /// [Test] public async Task Test_UnknownFilesystem_WriteRead_RoundTrip_DataMatches_Async() { // Create a partition on the writable disk var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; // Create UnknownFilesystem with 1MB block size using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // Get first file IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); // Write data to the file var writeData = new byte[1024 * 1024]; new Random(42).NextBytes(writeData); using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None)) { await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None); } // Read data back byte[] readData; using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None)) { readData = new byte[writeData.Length]; var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None); Assert.AreEqual(writeData.Length, bytesRead, "Should have read all bytes."); } // Verify data matches Assert.AreEqual(writeData, readData, "Read data should match written data."); } /// /// Tests that BoundsCheck validates ranges correctly. /// [Test] public void Test_UnknownFilesystem_BoundsCheck_ValidRange_DoesNotThrow() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // Valid ranges should not throw Assert.DoesNotThrow(() => fs.BoundsCheck(0, sectorSize), "BoundsCheck should not throw for valid range at start."); Assert.DoesNotThrow(() => fs.BoundsCheck(sectorSize, sectorSize), "BoundsCheck should not throw for valid range."); Assert.DoesNotThrow(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize), "BoundsCheck should not throw for valid range at end."); } /// /// Tests that BoundsCheck throws for negative start or size. /// [Test] public void Test_UnknownFilesystem_BoundsCheck_NegativeValues_ThrowsArgumentOutOfRange() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // Negative start should throw Assert.Throws(() => fs.BoundsCheck(-1, sectorSize), "Negative start should throw."); // Negative size should throw Assert.Throws(() => fs.BoundsCheck(0, -1), "Negative size should throw."); } /// /// Tests that BoundsCheck throws for ranges exceeding partition size. /// [Test] public void Test_UnknownFilesystem_BoundsCheck_ExceedsPartitionSize_ThrowsArgumentOutOfRange() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // Range exceeding partition size should throw Assert.Throws(() => fs.BoundsCheck(partitionSize - sectorSize, sectorSize * 2), "Range exceeding partition should throw."); Assert.Throws(() => fs.BoundsCheck(partitionSize, sectorSize), "Start at partition end should throw."); } /// /// Tests that BoundsCheck throws for unaligned start or size. /// [Test] public void Test_UnknownFilesystem_BoundsCheck_UnalignedValues_ThrowsArgumentException() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); // Unaligned start should throw Assert.Throws(() => fs.BoundsCheck(1, sectorSize), "Unaligned start should throw."); // Unaligned size should throw Assert.Throws(() => fs.BoundsCheck(0, sectorSize + 1), "Unaligned size should throw."); } /// /// Tests that UnknownFilesystem constructor throws for unaligned block sizes. /// [Test] public void Test_UnknownFilesystem_Constructor_UnalignedBlockSize_ThrowsArgumentException() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; // Block size not a multiple of sector size should throw var unalignedBlockSize = sectorSize + 1; var ex = Assert.Throws(() => new UnknownFilesystem(partition, blockSize: unalignedBlockSize), "Unaligned block size should throw ArgumentException."); Assert.That(ex!.ParamName, Is.EqualTo("blockSize"), "Exception should be for blockSize parameter."); } /// /// Tests that UnknownFilesystem constructor accepts valid block sizes. /// [Test] public void Test_UnknownFilesystem_Constructor_ValidBlockSize_CreatesInstance() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; // Valid block sizes should create instance Assert.DoesNotThrow(() => { using var fs1 = new UnknownFilesystem(partition, blockSize: sectorSize); }, "Block size equal to sector size should work."); Assert.DoesNotThrow(() => { using var fs2 = new UnknownFilesystem(partition, blockSize: sectorSize * 4); }, "Block size multiple of sector size should work."); Assert.DoesNotThrow(() => { using var fs3 = new UnknownFilesystem(partition, blockSize: 1024 * 1024); }, "1MB block size should work."); } /// /// Helper class to implement IPartition for testing. /// private class TestPartition : IPartition { public long StartOffset { get; init; } public long Size { get; init; } public IRawDisk? RawDisk { get; init; } public int PartitionNumber => 1; public string? Name => "TestPartition"; public Guid? UniqueId => Guid.Empty; public PartitionType Type => PartitionType.Primary; public FileSystemType FilesystemType => FileSystemType.Unknown; public Guid? VolumeGuid => Guid.Empty; public IPartitionTable PartitionTable => new TestPartitionTable { RawDisk = RawDisk }; public Task OpenReadAsync(CancellationToken cancellationToken) => Task.FromException(new NotImplementedException()); public Task OpenWriteAsync(CancellationToken cancellationToken) => Task.FromException(new NotImplementedException()); public void Dispose() { } } /// /// Helper class to implement IPartitionTable for testing. /// private class TestPartitionTable : IPartitionTable { public IRawDisk? RawDisk { get; init; } public PartitionTableType TableType => PartitionTableType.Unknown; public long Size => 0; public IAsyncEnumerable EnumeratePartitions(CancellationToken cancellationToken) => AsyncEnumerable.Empty(); public Task GetPartitionAsync(int partitionNumber, CancellationToken cancellationToken) => Task.FromResult(null); public Task GetProtectiveMbrAsync(CancellationToken cancellationToken) => Task.FromException(new NotImplementedException()); public Task GetPartitionTableDataAsync(CancellationToken cancellationToken) => Task.FromException(new NotImplementedException()); public void Dispose() { } } }