using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Duplicati.Proprietary.DiskImage.Filesystem; using Duplicati.Proprietary.DiskImage.General; using Duplicati.Proprietary.DiskImage.Partition; using NUnit.Framework; using Assert = NUnit.Framework.Legacy.ClassicAssert; using StringAssert = NUnit.Framework.Legacy.StringAssert; namespace Duplicati.UnitTest.DiskImage.UnitTests; #nullable enable public partial class DiskImageUnitTests : BasicSetupHelper { [Test] public async Task Test_GPT_GetProtectiveMbrAsync_ReturnsValidMbr_Async() { var sectorSize = s_gptRawDisk!.SectorSize; // Read enough sectors to parse GPT var sectorsToRead = 34; using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None); var diskBytes = new byte[sectorSize * sectorsToRead]; await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None); // Parse GPT var gpt = new GPT(s_gptRawDisk); var parsed = await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None); Assert.IsTrue(parsed, "GPT should parse successfully."); // Get protective MBR using var mbrStream = await gpt.GetProtectiveMbrAsync(CancellationToken.None); Assert.IsNotNull(mbrStream, "Protective MBR stream should not be null."); Assert.Greater(mbrStream.Length, 0, "Protective MBR stream should have data."); // Read MBR bytes and verify boot signature var mbrBytes = new byte[mbrStream.Length]; var bytesRead = await mbrStream.ReadAsync(mbrBytes.AsMemory(), CancellationToken.None); Assert.Greater(bytesRead, 0, "Should have read MBR bytes."); // Verify MBR boot signature (0x55AA at offset 510-511) if (bytesRead >= 512) { Assert.AreEqual(0x55, mbrBytes[510], "MBR boot signature first byte should be 0x55."); Assert.AreEqual(0xAA, mbrBytes[511], "MBR boot signature second byte should be 0xAA."); } gpt.Dispose(); } [Test] public async Task Test_GPT_GetPartitionTableDataAsync_ReturnsValidData_Async() { var sectorSize = s_gptRawDisk!.SectorSize; // Read enough sectors to parse GPT var sectorsToRead = 34; using var stream = await s_gptRawDisk.ReadSectorsAsync(0, sectorsToRead, CancellationToken.None); var diskBytes = new byte[sectorSize * sectorsToRead]; await stream.ReadAtLeastAsync(diskBytes, diskBytes.Length, cancellationToken: CancellationToken.None); // Parse GPT with raw disk reference var gpt = new GPT(s_gptRawDisk); var parsed = await gpt.ParseAsync(diskBytes, sectorSize, CancellationToken.None); Assert.IsTrue(parsed, "GPT should parse successfully."); // Get partition table data using var ptStream = await gpt.GetPartitionTableDataAsync(CancellationToken.None); Assert.IsNotNull(ptStream, "Partition table data stream should not be null."); Assert.Greater(ptStream.Length, 0, "Partition table data should have content."); // Read the data and verify it contains the GPT signature var ptBytes = new byte[ptStream.Length]; var bytesRead = await ptStream.ReadAsync(ptBytes.AsMemory(), CancellationToken.None); Assert.Greater(bytesRead, sectorSize, "Should have read more than one sector of partition table data."); // Verify GPT signature "EFI PART" at sector 1 (offset 512) var expectedSignature = "EFI PART"u8.ToArray(); for (int i = 0; i < 8; i++) Assert.AreEqual(expectedSignature[i], ptBytes[sectorSize + i], $"GPT signature byte {i} should match."); gpt.Dispose(); } [Test] public async Task Test_UnknownFilesystem_GetFilesystemMetadataAsync_ReturnsCorrectMetadata_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 1024 * 1024; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); var metadata = await fs.GetFilesystemMetadataAsync(CancellationToken.None); Assert.IsNotNull(metadata, "Metadata should not be null."); Assert.IsInstanceOf(metadata, "Metadata should be UnkownFilesystemMetadata."); var fsMetadata = (UnkownFilesystemMetadata)metadata!; Assert.AreEqual(blockSize, fsMetadata.BlockSize, "BlockSize should match the configured value."); } [Test] public async Task Test_UnknownFilesystem_GetFileLengthAsync_ReturnsCorrectSize_Async() { 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); // 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."); var length = await fs.GetFileLengthAsync(firstFile!, CancellationToken.None); Assert.AreEqual(firstFile!.Size, length, "GetFileLengthAsync should return the file's size."); Assert.AreEqual(1024 * 1024, length, "First file should be 1MB (full block)."); } [Test] public async Task Test_UnknownFilesystemStream_SetLength_ThrowsNotSupported_Async() { 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); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None); Assert.Throws(() => writeStream.SetLength(100), "SetLength should throw NotSupportedException on UnknownFilesystemStream."); } [Test] public async Task Test_UnknownFilesystemStream_WriteOnReadOnly_ThrowsNotSupported_Async() { 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); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); Assert.IsFalse(readStream.CanWrite, "Read stream should not be writable."); Assert.Throws(() => readStream.Write(new byte[10], 0, 10), "Write on read-only stream should throw NotSupportedException."); } [Test] public async Task Test_UnknownFilesystemStream_ReadOnWriteOnly_ThrowsNotSupported_Async() { 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); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None); Assert.IsFalse(writeStream.CanRead, "Write stream should not be readable."); Assert.Throws(() => { var _ = writeStream.Read(new byte[10], 0, 10); }, "Read on write-only stream should throw NotSupportedException."); } [Test] public async Task Test_UnknownFilesystemStream_DisposeAsync_FlushesDirtyData_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = sectorSize * 2; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); 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 using DisposeAsync (await using) var writeData = new byte[blockSize]; new Random(99).NextBytes(writeData); await using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None)) { await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None); } // Read back and verify data was flushed via DisposeAsync using (var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None)) { var readData = new byte[blockSize]; var bytesRead = await readStream.ReadAsync(readData.AsMemory(), CancellationToken.None); Assert.AreEqual(blockSize, bytesRead, "Should have read the full block."); Assert.AreEqual(writeData, readData, "Data should match after DisposeAsync flush."); } } [Test] public void Test_UnknownFilesystem_DoubleDispose_DoesNotThrow() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var fs = new UnknownFilesystem(partition, blockSize: 1024 * 1024); fs.Dispose(); // Second dispose should not throw Assert.DoesNotThrow(() => fs.Dispose(), "Double dispose should not throw."); } [Test] public void Test_UnknownFilesystem_Type_ReturnsUnknown() { 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); Assert.AreEqual(FileSystemType.Unknown, fs.Type, "Type should be Unknown."); Assert.AreEqual(partition, fs.Partition, "Partition should match the one passed to constructor."); } [Test] public async Task Test_UnknownFilesystemStream_PositionSetter_ValidatesBounds_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 4096; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); // Valid position readStream.Position = 100; Assert.AreEqual(100, readStream.Position, "Position should be 100."); // Position at end (equal to size) should be valid readStream.Position = blockSize; Assert.AreEqual(blockSize, readStream.Position, "Position at end should be valid."); // Negative position should throw Assert.Throws(() => readStream.Position = -1, "Negative position should throw ArgumentOutOfRangeException."); // Position beyond size should throw Assert.Throws(() => readStream.Position = blockSize + 1, "Position beyond size should throw ArgumentOutOfRangeException."); } [Test] public async Task Test_UnknownFilesystemStream_Flush_DoesNotThrow_Async() { 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); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None); Assert.DoesNotThrow(() => writeStream.Flush(), "Flush should be a no-op and not throw."); } [Test] public void Test_PartitionWriteStream_Flush_DoesNotThrow() { var sectorSize = _writableRawDisk.SectorSize; using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize); Assert.DoesNotThrow(() => stream.Flush(), "Flush should be a no-op and not throw."); } [Test] public async Task Test_UnknownFilesystemStream_WriteBeyondSize_ThrowsArgumentException_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 4096; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None); // Try to write more data than the stream size var largeData = new byte[blockSize + 100]; Assert.Throws(() => writeStream.Write(largeData, 0, largeData.Length), "Writing beyond stream size should throw ArgumentException."); } [Test] public async Task Test_UnknownFilesystemStream_SyncRead_ReturnsCorrectData_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 4096; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); 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 test data var writeData = new byte[blockSize]; for (int i = 0; i < blockSize; i++) writeData[i] = (byte)(i & 0xFF); using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None)) await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None); // Read using synchronous Read method using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); var readBuffer = new byte[256]; var bytesRead = await readStream.ReadAsync(readBuffer, 0, 256); Assert.AreEqual(256, bytesRead, "Sync Read should return 256 bytes."); for (int i = 0; i < 256; i++) Assert.AreEqual((byte)(i & 0xFF), readBuffer[i], $"Byte at position {i} should match."); } [Test] public async Task Test_UnknownFilesystemStream_SyncWrite_DataMatches_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 4096; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); 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 using synchronous Write method var writeData = new byte[100]; new Random(42).NextBytes(writeData); using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None)) await writeStream.WriteAsync(writeData, 0, writeData.Length); // Read back and verify using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); var readBuffer = new byte[100]; var bytesRead = await readStream.ReadAsync(readBuffer, 0, 100); Assert.AreEqual(100, bytesRead, "Should read 100 bytes."); Assert.AreEqual(writeData, readBuffer, "Sync-written data should match on read-back."); } [Test] public void Test_PooledMemoryStream_Flush_DoesNotThrow() { byte[] buffer; using (var pooledBuffer = new PooledBuffer(100)) buffer = pooledBuffer.Array; using var stream = new PooledMemoryStream(buffer, 100); Assert.DoesNotThrow(() => stream.Flush(), "Flush should be a no-op and not throw."); } [Test] public void Test_PooledBuffer_SpanAndMemory_ReturnCorrectSize() { using var buffer = new PooledBuffer(512); Assert.AreEqual(512, buffer.Span.Length, "Span length should match requested size."); Assert.AreEqual(512, buffer.Memory.Length, "Memory length should match requested size."); // Write via Span, read via Memory buffer.Span[0] = 0xAA; buffer.Span[511] = 0xBB; Assert.AreEqual(0xAA, buffer.Memory.Span[0], "Memory should reflect Span writes."); Assert.AreEqual(0xBB, buffer.Memory.Span[511], "Memory should reflect Span writes."); } /// /// Tests that ReconstructedPartitionTable stores the correct table type and raw disk reference. /// [Test] public void Test_ReconstructedPartitionTable_Constructor_SetsProperties() { var metadata = new GeometryMetadata { Disk = new DiskGeometry { SectorSize = 512, Size = 50 * MiB }, PartitionTable = new PartitionTableGeometry { Type = PartitionTableType.GPT } }; using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); Assert.AreEqual(PartitionTableType.GPT, table.TableType, "TableType should be GPT."); Assert.AreEqual(_writableRawDisk, table.RawDisk, "RawDisk should match the provided disk."); } /// /// Tests that ReconstructedPartitionTable.EnumeratePartitions throws NotSupportedException. /// [Test] public void Test_ReconstructedPartitionTable_EnumeratePartitions_ThrowsNotSupported() { var metadata = new GeometryMetadata(); using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); Assert.ThrowsAsync(async () => { // Force enumeration to trigger the throw var enumerator = table.EnumeratePartitions(CancellationToken.None).GetAsyncEnumerator(); await enumerator.MoveNextAsync().AsTask(); }, "EnumeratePartitions should throw NotSupportedException."); } /// /// Tests that ReconstructedPartitionTable.GetPartitionAsync throws NotSupportedException. /// [Test] public void Test_ReconstructedPartitionTable_GetPartitionAsync_ThrowsNotSupported() { var metadata = new GeometryMetadata(); using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); Assert.ThrowsAsync(async () => { await table.GetPartitionAsync(1, CancellationToken.None); }, "GetPartitionAsync should throw NotSupportedException."); } /// /// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for GPT. /// [Test] public void Test_ReconstructedPartitionTable_GetProtectiveMbrAsync_GPT_ThrowsNotSupported() { var metadata = new GeometryMetadata(); using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); Assert.ThrowsAsync(async () => { await table.GetProtectiveMbrAsync(CancellationToken.None); }, "GetProtectiveMbrAsync should throw NotSupportedException for GPT."); } /// /// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for MBR. /// [Test] public void Test_ReconstructedPartitionTable_GetProtectiveMbrAsync_MBR_ThrowsNotSupported() { var metadata = new GeometryMetadata(); using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.MBR); var ex = Assert.ThrowsAsync(async () => { await table.GetProtectiveMbrAsync(CancellationToken.None); }, "GetProtectiveMbrAsync should throw NotSupportedException for MBR."); StringAssert.Contains("MBR", ex!.Message, "Exception message should mention MBR."); } /// /// Tests that ReconstructedPartitionTable.GetPartitionTableDataAsync throws NotSupportedException. /// [Test] public void Test_ReconstructedPartitionTable_GetPartitionTableDataAsync_ThrowsNotSupported() { var metadata = new GeometryMetadata(); using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); Assert.ThrowsAsync(async () => { await table.GetPartitionTableDataAsync(CancellationToken.None); }, "GetPartitionTableDataAsync should throw NotSupportedException."); } /// /// Tests that ReconstructedPartitionTable.Dispose can be called multiple times without error. /// [Test] public void Test_ReconstructedPartitionTable_DoubleDispose_DoesNotThrow() { var metadata = new GeometryMetadata(); var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT); table.Dispose(); Assert.DoesNotThrow(() => table.Dispose(), "Double dispose should not throw."); } /// /// Tests that BasePartition.OpenReadAsync returns a readable stream. /// [Test] public async Task Test_BasePartition_OpenReadAsync_ReturnsReadableStream_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionOffset = 10L * sectorSize; var partitionSize = 4L * sectorSize; // Write known data to the partition area var writeData = new byte[sectorSize]; for (int i = 0; i < sectorSize; i++) writeData[i] = (byte)(i & 0xFF); await _writableRawDisk.WriteBytesAsync(partitionOffset, writeData, CancellationToken.None); var partition = new BasePartition { PartitionNumber = 1, Type = PartitionType.Primary, PartitionTable = new TestPartitionTable { RawDisk = _writableRawDisk }, StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var stream = await partition.OpenReadAsync(CancellationToken.None); Assert.IsNotNull(stream, "OpenReadAsync should return a stream."); Assert.IsTrue(stream.CanRead, "Stream should be readable."); var readBuffer = new byte[sectorSize]; var bytesRead = await stream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None); Assert.AreEqual(sectorSize, bytesRead, "Should read one sector."); Assert.AreEqual(writeData, readBuffer, "Data should match what was written."); partition.Dispose(); } /// /// Tests that BasePartition.OpenWriteAsync returns a writable PartitionWriteStream. /// [Test] public async Task Test_BasePartition_OpenWriteAsync_ReturnsWritableStream_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionOffset = 10L * sectorSize; var partitionSize = 4L * sectorSize; var partition = new BasePartition { PartitionNumber = 1, Type = PartitionType.Primary, PartitionTable = new TestPartitionTable { RawDisk = _writableRawDisk }, StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; using var stream = await partition.OpenWriteAsync(CancellationToken.None); Assert.IsNotNull(stream, "OpenWriteAsync should return a stream."); Assert.IsTrue(stream.CanWrite, "Stream should be writable."); Assert.IsInstanceOf(stream, "Stream should be a PartitionWriteStream."); partition.Dispose(); } /// /// Tests that UnknownPartitionTable.GetProtectiveMbrAsync throws NotSupportedException. /// [Test] public void Test_UnknownPartitionTable_GetProtectiveMbrAsync_ThrowsNotSupported() { using var table = new UnknownPartitionTable(null); Assert.ThrowsAsync(async () => { await table.GetProtectiveMbrAsync(CancellationToken.None); }, "GetProtectiveMbrAsync should throw NotSupportedException."); } /// /// Tests that UnknownPartitionTable.GetPartitionTableDataAsync returns empty data. /// [Test] public async Task Test_UnknownPartitionTable_GetPartitionTableDataAsync_ReturnsEmptyStream_Async() { using var table = new UnknownPartitionTable(null); using var stream = await table.GetPartitionTableDataAsync(CancellationToken.None); Assert.IsNotNull(stream, "Stream should not be null."); Assert.AreEqual(0, stream.Length, "Stream should be empty."); } /// /// Tests that UnknownPartitionTable.Dispose can be called multiple times. /// [Test] public void Test_UnknownPartitionTable_DoubleDispose_DoesNotThrow() { var table = new UnknownPartitionTable(null); table.Dispose(); Assert.DoesNotThrow(() => table.Dispose(), "Double dispose should not throw."); } /// /// Tests that UnknownPartitionTable.GetPartitionAsync returns null for invalid partition numbers. /// [Test] public async Task Test_UnknownPartitionTable_GetPartitionAsync_InvalidNumber_ReturnsNull_Async() { using var table = new UnknownPartitionTable(null); var result = await table.GetPartitionAsync(2, CancellationToken.None); Assert.IsNull(result, "Should return null for partition number != 1."); var result0 = await table.GetPartitionAsync(0, CancellationToken.None); Assert.IsNull(result0, "Should return null for partition number 0."); } /// /// Tests that UnknownPartitionTable.GetPartitionAsync returns null when no raw disk is set. /// [Test] public async Task Test_UnknownPartitionTable_GetPartitionAsync_NullDisk_ReturnsNull_Async() { using var table = new UnknownPartitionTable(null); var result = await table.GetPartitionAsync(1, CancellationToken.None); Assert.IsNull(result, "Should return null when raw disk is null."); } /// /// Tests that UnknownPartitionTable.GetPartitionAsync returns a valid partition when disk is set. /// [Test] public async Task Test_UnknownPartitionTable_GetPartitionAsync_ValidDisk_ReturnsPartition_Async() { using var table = new UnknownPartitionTable(_writableRawDisk); var result = await table.GetPartitionAsync(1, CancellationToken.None); Assert.IsNotNull(result, "Should return a partition when disk is available."); Assert.AreEqual(1, result!.PartitionNumber, "Partition number should be 1."); Assert.AreEqual(_writableRawDisk.Size, result.Size, "Partition size should equal disk size."); Assert.AreEqual(0, result.StartOffset, "Partition start offset should be 0."); result.Dispose(); } /// /// Tests that PooledMemoryStream.Position setter validates bounds correctly. /// [Test] public void Test_PooledMemoryStream_PositionSetter_ValidatesBounds() { byte[] buffer; using (var pooledBuffer = new PooledBuffer(100)) buffer = pooledBuffer.Array; using var stream = new PooledMemoryStream(buffer, 100); // Valid position stream.Position = 50; Assert.AreEqual(50, stream.Position, "Position should be 50."); // Position at end stream.Position = 100; Assert.AreEqual(100, stream.Position, "Position at end should be valid."); // Position at start stream.Position = 0; Assert.AreEqual(0, stream.Position, "Position at start should be valid."); // Negative position should throw Assert.Throws(() => stream.Position = -1, "Negative position should throw."); // Position beyond length should throw Assert.Throws(() => stream.Position = 101, "Position beyond length should throw."); } /// /// Tests that PooledMemoryStream.Seek with invalid origin throws ArgumentOutOfRangeException. /// [Test] public void Test_PooledMemoryStream_Seek_InvalidOrigin_ThrowsArgumentOutOfRange() { byte[] buffer; using (var pooledBuffer = new PooledBuffer(100)) buffer = pooledBuffer.Array; using var stream = new PooledMemoryStream(buffer, 100); Assert.Throws(() => stream.Seek(0, (SeekOrigin)99), "Invalid SeekOrigin should throw ArgumentOutOfRangeException."); } /// /// Tests that PartitionWriteStream.Position setter validates bounds correctly. /// [Test] public void Test_PartitionWriteStream_PositionSetter_ValidatesBounds() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 2 * sectorSize; using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize); // Valid position stream.Position = 100; Assert.AreEqual(100, stream.Position, "Position should be 100."); // Position at max size stream.Position = partitionSize; Assert.AreEqual(partitionSize, stream.Position, "Position at max size should be valid."); // Negative position should throw Assert.Throws(() => stream.Position = -1, "Negative position should throw."); // Position beyond max size should throw Assert.Throws(() => stream.Position = partitionSize + 1, "Position beyond max size should throw."); } /// /// Tests that PartitionWriteStream.Seek with invalid origin throws ArgumentOutOfRangeException. /// [Test] public void Test_PartitionWriteStream_Seek_InvalidOrigin_ThrowsArgumentOutOfRange() { var sectorSize = _writableRawDisk.SectorSize; using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize); Assert.Throws(() => stream.Seek(0, (SeekOrigin)99), "Invalid SeekOrigin should throw ArgumentOutOfRangeException."); } /// /// Tests that PartitionWriteStream.SetLength adjusts position when it exceeds new length. /// [Test] public void Test_PartitionWriteStream_SetLength_AdjustsPosition() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 4 * sectorSize; using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, partitionSize); // Write some data to move position stream.Write(new byte[200], 0, 200); Assert.AreEqual(200, stream.Position, "Position should be 200."); // Set length to less than current position stream.SetLength(100); Assert.AreEqual(100, stream.Length, "Length should be 100."); Assert.AreEqual(100, stream.Position, "Position should be adjusted to new length."); } /// /// Tests that UnknownFilesystem path-based OpenReadStreamAsync works correctly. /// [Test] public async Task Test_UnknownFilesystem_OpenReadStreamAsync_ByPath_ReturnsStream_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 1024 * 1024; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); // Write data first using IFile-based API IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); var writeData = new byte[100]; new Random(42).NextBytes(writeData); using (var writeStream = await fs.OpenWriteStreamAsync(firstFile!, CancellationToken.None)) await writeStream.WriteAsync(writeData.AsMemory(), CancellationToken.None); // Read using path-based API var path = $"root/part_Unknown_1/fs_Unknown/{((UnknownFilesystemFile)firstFile!).Address:X}"; using var readStream = await fs.OpenReadStreamAsync(path, CancellationToken.None); Assert.IsNotNull(readStream, "Path-based OpenReadStreamAsync should return a stream."); var readBuffer = new byte[100]; var bytesRead = await readStream.ReadAsync(readBuffer.AsMemory(), CancellationToken.None); Assert.AreEqual(100, bytesRead, "Should read 100 bytes."); Assert.AreEqual(writeData, readBuffer, "Data should match."); } /// /// Tests that UnknownFilesystem path-based OpenWriteStreamAsync works correctly. /// [Test] public async Task Test_UnknownFilesystem_OpenWriteStreamAsync_ByPath_ReturnsStream_Async() { 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); // Write using path-based API var path = $"root/part_Unknown_1/fs_Unknown/0"; using var writeStream = await fs.OpenWriteStreamAsync(path, CancellationToken.None); Assert.IsNotNull(writeStream, "Path-based OpenWriteStreamAsync should return a stream."); Assert.IsTrue(writeStream.CanWrite, "Stream should be writable."); } /// /// Tests that UnknownFilesystem path-based OpenReadWriteStreamAsync works correctly. /// [Test] public async Task Test_UnknownFilesystem_OpenReadWriteStreamAsync_ByPath_ReturnsStream_Async() { 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); // Open read-write using path-based API var path = $"root/part_Unknown_1/fs_Unknown/0"; using var stream = await fs.OpenReadWriteStreamAsync(path, CancellationToken.None); Assert.IsNotNull(stream, "Path-based OpenReadWriteStreamAsync should return a stream."); Assert.IsTrue(stream.CanRead, "Stream should be readable."); Assert.IsTrue(stream.CanWrite, "Stream should be writable."); } /// /// Tests that UnknownFilesystem path-based GetFileLengthAsync returns correct size. /// [Test] public async Task Test_UnknownFilesystem_GetFileLengthAsync_ByPath_ReturnsCorrectSize_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 1024 * 1024; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); // Get file length using path-based API var path = $"root/part_Unknown_1/fs_Unknown/0"; var length = await fs.GetFileLengthAsync(path, CancellationToken.None); Assert.AreEqual(blockSize, length, "File length should equal block size for first block."); } /// /// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile (including directories). /// [Test] public void Test_UnknownFilesystem_GetFileLengthAsync_NonUnknownFile_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); // A directory file that is not UnknownFilesystemFile should throw "does not belong" var dirFile = new MockDirectoryFile(); var ex = Assert.ThrowsAsync(async () => { await fs.GetFileLengthAsync(dirFile, CancellationToken.None); }, "GetFileLengthAsync should throw for non-UnknownFilesystemFile."); StringAssert.Contains("does not belong", ex!.Message, "Exception message should mention file doesn't belong."); } /// /// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile. /// [Test] public void Test_UnknownFilesystem_GetFileLengthAsync_WrongFileType_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); // Create a mock file that is not UnknownFilesystemFile var wrongFile = new MockNonUnknownFile(); var ex = Assert.ThrowsAsync(async () => { await fs.GetFileLengthAsync(wrongFile, CancellationToken.None); }, "GetFileLengthAsync should throw for non-UnknownFilesystemFile."); StringAssert.Contains("does not belong", ex!.Message, "Exception message should mention file doesn't belong."); } /// /// Tests that UnknownFilesystemStream.Seek with invalid origin throws ArgumentException. /// [Test] public async Task Test_UnknownFilesystemStream_Seek_InvalidOrigin_ThrowsArgumentException_Async() { 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: 4096); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var stream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); Assert.Throws(() => stream.Seek(0, (SeekOrigin)99), "Invalid SeekOrigin should throw ArgumentException."); } /// /// Tests that UnknownFilesystemStream.Seek beyond bounds throws ArgumentOutOfRangeException. /// [Test] public async Task Test_UnknownFilesystemStream_Seek_BeyondBounds_ThrowsArgumentOutOfRange_Async() { var sectorSize = _writableRawDisk.SectorSize; var partitionSize = 5 * MiB; var partitionOffset = sectorSize * 2048; var partition = new TestPartition { StartOffset = partitionOffset, Size = partitionSize, RawDisk = _writableRawDisk }; var blockSize = 4096; using var fs = new UnknownFilesystem(partition, blockSize: blockSize); IFile? firstFile = null; await foreach (var file in fs.ListFilesAsync(CancellationToken.None)) { firstFile = file; break; } Assert.IsNotNull(firstFile, "Should have at least one file."); using var stream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None); // Seek before beginning Assert.Throws(() => stream.Seek(-1, SeekOrigin.Begin), "Seek before beginning should throw."); // Seek past end Assert.Throws(() => stream.Seek(blockSize + 1, SeekOrigin.Begin), "Seek past end should throw."); } /// /// Tests that UnknownFilesystem.ListFilesAsync with a non-UnknownFilesystemFile directory throws ArgumentException. /// [Test] public void Test_UnknownFilesystem_ListFilesAsync_WithNonUnknownFile_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); // ListFilesAsync with a non-UnknownFilesystemFile should throw var dirFile = new MockDirectoryFile(); Assert.ThrowsAsync(async () => { await foreach (var file in fs.ListFilesAsync(dirFile, CancellationToken.None)) { } }, "ListFilesAsync should throw for non-UnknownFilesystemFile."); } /// /// Tests that UnknownFilesystem.ListFilesAsync with an UnknownFilesystemFile (non-directory) throws ArgumentException. /// [Test] public void Test_UnknownFilesystem_ListFilesAsync_WithNonDirectory_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); // UnknownFilesystemFile.IsDirectory is always false, so this should throw "not a directory" var nonDirFile = new UnknownFilesystemFile { Address = 0, Size = 1024 * 1024 }; Assert.ThrowsAsync(async () => { await foreach (var file in fs.ListFilesAsync(nonDirFile, CancellationToken.None)) { } }, "ListFilesAsync should throw for non-directory UnknownFilesystemFile."); } /// /// Tests that UnknownFilesystem.ParsePathToAddress throws for invalid path format. /// [Test] public async Task Test_UnknownFilesystem_OpenReadStreamAsync_InvalidPath_ThrowsArgumentException_Async() { 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); // Path with too few segments should throw Assert.ThrowsAsync(async () => { await fs.OpenReadStreamAsync("invalid", CancellationToken.None); }, "Invalid path should throw ArgumentException."); } /// /// Mock directory file for testing. /// private class MockDirectoryFile : IFile { public string? Path => "/dir"; public long? Address => null; public long Size => 0; public bool IsDirectory => true; public DateTime? LastModified => null; } /// /// Mock non-UnknownFilesystemFile for testing. /// private class MockNonUnknownFile : IFile { public string? Path => "/file.txt"; public long? Address => 0; public long Size => 100; public bool IsDirectory => false; public DateTime? LastModified => null; } }