1272 lines
48 KiB
C#
1272 lines
48 KiB
C#
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()
|
|
{
|
|
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()
|
|
{
|
|
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()
|
|
{
|
|
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<UnkownFilesystemMetadata>(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()
|
|
{
|
|
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()
|
|
{
|
|
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<NotSupportedException>(() => writeStream.SetLength(100),
|
|
"SetLength should throw NotSupportedException on UnknownFilesystemStream.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_WriteOnReadOnly_ThrowsNotSupported()
|
|
{
|
|
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<NotSupportedException>(() => readStream.Write(new byte[10], 0, 10),
|
|
"Write on read-only stream should throw NotSupportedException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_ReadOnWriteOnly_ThrowsNotSupported()
|
|
{
|
|
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<NotSupportedException>(() => { var _ = writeStream.Read(new byte[10], 0, 10); },
|
|
"Read on write-only stream should throw NotSupportedException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_DisposeAsync_FlushesDirtyData()
|
|
{
|
|
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()
|
|
{
|
|
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<ArgumentOutOfRangeException>(() => readStream.Position = -1,
|
|
"Negative position should throw ArgumentOutOfRangeException.");
|
|
|
|
// Position beyond size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => readStream.Position = blockSize + 1,
|
|
"Position beyond size should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Flush_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);
|
|
|
|
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()
|
|
{
|
|
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<ArgumentException>(() => writeStream.Write(largeData, 0, largeData.Length),
|
|
"Writing beyond stream size should throw ArgumentException.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_SyncRead_ReturnsCorrectData()
|
|
{
|
|
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 = readStream.Read(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()
|
|
{
|
|
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))
|
|
writeStream.Write(writeData, 0, writeData.Length);
|
|
|
|
// Read back and verify
|
|
using var readStream = await fs.OpenReadStreamAsync(firstFile!, CancellationToken.None);
|
|
var readBuffer = new byte[100];
|
|
var bytesRead = readStream.Read(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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable stores the correct table type and raw disk reference.
|
|
/// </summary>
|
|
[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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.EnumeratePartitions throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_EnumeratePartitions_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.Throws<NotSupportedException>(() =>
|
|
{
|
|
// Force enumeration to trigger the throw
|
|
var enumerator = table.EnumeratePartitions(CancellationToken.None).GetAsyncEnumerator();
|
|
enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult();
|
|
}, "EnumeratePartitions should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetPartitionAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetPartitionAsync_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetPartitionAsync(1, CancellationToken.None);
|
|
}, "GetPartitionAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for GPT.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetProtectiveMbrAsync_GPT_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException for GPT.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetProtectiveMbrAsync throws NotSupportedException for MBR.
|
|
/// </summary>
|
|
[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<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException for MBR.");
|
|
|
|
StringAssert.Contains("MBR", ex!.Message, "Exception message should mention MBR.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.GetPartitionTableDataAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_ReconstructedPartitionTable_GetPartitionTableDataAsync_ThrowsNotSupported()
|
|
{
|
|
var metadata = new GeometryMetadata();
|
|
using var table = new ReconstructedPartitionTable(_writableRawDisk, metadata, PartitionTableType.GPT);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetPartitionTableDataAsync(CancellationToken.None);
|
|
}, "GetPartitionTableDataAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that ReconstructedPartitionTable.Dispose can be called multiple times without error.
|
|
/// </summary>
|
|
[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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BasePartition.OpenReadAsync returns a readable stream.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_BasePartition_OpenReadAsync_ReturnsReadableStream()
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that BasePartition.OpenWriteAsync returns a writable PartitionWriteStream.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_BasePartition_OpenWriteAsync_ReturnsWritableStream()
|
|
{
|
|
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<PartitionWriteStream>(stream, "Stream should be a PartitionWriteStream.");
|
|
|
|
partition.Dispose();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetProtectiveMbrAsync throws NotSupportedException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownPartitionTable_GetProtectiveMbrAsync_ThrowsNotSupported()
|
|
{
|
|
using var table = new UnknownPartitionTable(null);
|
|
|
|
Assert.ThrowsAsync<NotSupportedException>(async () =>
|
|
{
|
|
await table.GetProtectiveMbrAsync(CancellationToken.None);
|
|
}, "GetProtectiveMbrAsync should throw NotSupportedException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionTableDataAsync returns empty data.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionTableDataAsync_ReturnsEmptyStream()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.Dispose can be called multiple times.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_UnknownPartitionTable_DoubleDispose_DoesNotThrow()
|
|
{
|
|
var table = new UnknownPartitionTable(null);
|
|
table.Dispose();
|
|
Assert.DoesNotThrow(() => table.Dispose(), "Double dispose should not throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns null for invalid partition numbers.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_InvalidNumber_ReturnsNull()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns null when no raw disk is set.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_NullDisk_ReturnsNull()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownPartitionTable.GetPartitionAsync returns a valid partition when disk is set.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownPartitionTable_GetPartitionAsync_ValidDisk_ReturnsPartition()
|
|
{
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream.Position setter validates bounds correctly.
|
|
/// </summary>
|
|
[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<ArgumentOutOfRangeException>(() => stream.Position = -1,
|
|
"Negative position should throw.");
|
|
|
|
// Position beyond length should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = 101,
|
|
"Position beyond length should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PooledMemoryStream.Seek with invalid origin throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[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<ArgumentOutOfRangeException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.Position setter validates bounds correctly.
|
|
/// </summary>
|
|
[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<ArgumentOutOfRangeException>(() => stream.Position = -1,
|
|
"Negative position should throw.");
|
|
|
|
// Position beyond max size should throw
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = partitionSize + 1,
|
|
"Position beyond max size should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.Seek with invalid origin throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public void Test_PartitionWriteStream_Seek_InvalidOrigin_ThrowsArgumentOutOfRange()
|
|
{
|
|
var sectorSize = _writableRawDisk.SectorSize;
|
|
using var stream = new PartitionWriteStream(_writableRawDisk, 10 * sectorSize, sectorSize);
|
|
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentOutOfRangeException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that PartitionWriteStream.SetLength adjusts position when it exceeds new length.
|
|
/// </summary>
|
|
[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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenReadStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenWriteStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenWriteStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based OpenReadWriteStreamAsync works correctly.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadWriteStreamAsync_ByPath_ReturnsStream()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem path-based GetFileLengthAsync returns correct size.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_GetFileLengthAsync_ByPath_ReturnsCorrectSize()
|
|
{
|
|
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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile (including directories).
|
|
/// </summary>
|
|
[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<ArgumentException>(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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.GetFileLengthAsync throws for non-UnknownFilesystemFile.
|
|
/// </summary>
|
|
[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<ArgumentException>(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.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystemStream.Seek with invalid origin throws ArgumentException.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Seek_InvalidOrigin_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: 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<ArgumentException>(() => stream.Seek(0, (SeekOrigin)99),
|
|
"Invalid SeekOrigin should throw ArgumentException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystemStream.Seek beyond bounds throws ArgumentOutOfRangeException.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystemStream_Seek_BeyondBounds_ThrowsArgumentOutOfRange()
|
|
{
|
|
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<ArgumentOutOfRangeException>(() => stream.Seek(-1, SeekOrigin.Begin),
|
|
"Seek before beginning should throw.");
|
|
|
|
// Seek past end
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Seek(blockSize + 1, SeekOrigin.Begin),
|
|
"Seek past end should throw.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ListFilesAsync with a non-UnknownFilesystemFile directory throws ArgumentException.
|
|
/// </summary>
|
|
[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<ArgumentException>(async () =>
|
|
{
|
|
await foreach (var file in fs.ListFilesAsync(dirFile, CancellationToken.None))
|
|
{ }
|
|
}, "ListFilesAsync should throw for non-UnknownFilesystemFile.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ListFilesAsync with an UnknownFilesystemFile (non-directory) throws ArgumentException.
|
|
/// </summary>
|
|
[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<ArgumentException>(async () =>
|
|
{
|
|
await foreach (var file in fs.ListFilesAsync(nonDirFile, CancellationToken.None))
|
|
{ }
|
|
}, "ListFilesAsync should throw for non-directory UnknownFilesystemFile.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests that UnknownFilesystem.ParsePathToAddress throws for invalid path format.
|
|
/// </summary>
|
|
[Test]
|
|
public async Task Test_UnknownFilesystem_OpenReadStreamAsync_InvalidPath_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);
|
|
|
|
// Path with too few segments should throw
|
|
Assert.ThrowsAsync<ArgumentException>(async () =>
|
|
{
|
|
await fs.OpenReadStreamAsync("invalid", CancellationToken.None);
|
|
}, "Invalid path should throw ArgumentException.");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock directory file for testing.
|
|
/// </summary>
|
|
private class MockDirectoryFile : IFile
|
|
{
|
|
public string? Path => "/dir";
|
|
public long? Address => null;
|
|
public long Size => 0;
|
|
public bool IsDirectory => true;
|
|
public DateTime? LastModified => null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mock non-UnknownFilesystemFile for testing.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
} |